5 var EventEmitter = require('events').EventEmitter;
6 var spawn = require('child_process').spawn;
7 var readlink = require('graceful-readlink').readlinkSync;
8 var path = require('path');
9 var dirname = path.dirname;
10 var basename = path.basename;
11 var fs = require('fs');
14 * Expose the root command.
17 exports = module.exports = new Command();
23 exports.Command = Command;
29 exports.Option = Option;
32 * Initialize a new `Option` with the given `flags` and `description`.
34 * @param {String} flags
35 * @param {String} description
39 function Option(flags, description) {
41 this.required = ~flags.indexOf('<');
42 this.optional = ~flags.indexOf('[');
43 this.bool = !~flags.indexOf('-no-');
44 flags = flags.split(/[ ,|]+/);
45 if (flags.length > 1 && !/^[[<]/.test(flags[1])) this.short = flags.shift();
46 this.long = flags.shift();
47 this.description = description || '';
57 Option.prototype.name = function() {
64 * Check if `arg` matches the short or long flag.
71 Option.prototype.is = function(arg) {
72 return arg == this.short || arg == this.long;
76 * Initialize a new `Command`.
78 * @param {String} name
82 function Command(name) {
86 this._allowUnknownOption = false;
88 this._name = name || '';
92 * Inherit from `EventEmitter.prototype`.
95 Command.prototype.__proto__ = EventEmitter.prototype;
100 * The `.action()` callback is invoked when the
101 * command `name` is specified via __ARGV__,
102 * and the remaining arguments are applied to the
103 * function for access.
105 * When the `name` is "*" an un-matched command
106 * will be passed as the first arg, followed by
107 * the rest of __ARGV__ remaining.
113 * .option('-C, --chdir <path>', 'change the working directory')
114 * .option('-c, --config <path>', 'set config path. defaults to ./deploy.conf')
115 * .option('-T, --no-tests', 'ignore test hook')
119 * .description('run remote setup commands')
120 * .action(function() {
121 * console.log('setup');
125 * .command('exec <cmd>')
126 * .description('run the given remote command')
127 * .action(function(cmd) {
128 * console.log('exec "%s"', cmd);
132 * .command('teardown <dir> [otherDirs...]')
133 * .description('run teardown commands')
134 * .action(function(dir, otherDirs) {
135 * console.log('dir "%s"', dir);
137 * otherDirs.forEach(function (oDir) {
138 * console.log('dir "%s"', oDir);
145 * .description('deploy the given env')
146 * .action(function(env) {
147 * console.log('deploying "%s"', env);
150 * program.parse(process.argv);
152 * @param {String} name
153 * @param {String} [desc] for git-style sub-commands
154 * @return {Command} the new command
158 Command.prototype.command = function(name, desc, opts) {
160 var args = name.split(/ +/);
161 var cmd = new Command(args.shift());
164 cmd.description(desc);
165 this.executables = true;
166 this._execs[cmd._name] = true;
167 if (opts.isDefault) this.defaultExecutable = cmd._name;
170 cmd._noHelp = !!opts.noHelp;
171 this.commands.push(cmd);
172 cmd.parseExpectedArgs(args);
175 if (desc) return this;
180 * Define argument syntax for the top-level command.
185 Command.prototype.arguments = function (desc) {
186 return this.parseExpectedArgs(desc.split(/ +/));
190 * Add an implicit `help [cmd]` subcommand
191 * which invokes `--help` for the given command.
196 Command.prototype.addImplicitHelpCommand = function() {
197 this.command('help [cmd]', 'display help for [cmd]');
201 * Parse expected `args`.
203 * For example `["[type]"]` becomes `[{ required: false, name: 'type' }]`.
205 * @param {Array} args
206 * @return {Command} for chaining
210 Command.prototype.parseExpectedArgs = function(args) {
211 if (!args.length) return;
213 args.forEach(function(arg) {
222 argDetails.required = true;
223 argDetails.name = arg.slice(1, -1);
226 argDetails.name = arg.slice(1, -1);
230 if (argDetails.name.length > 3 && argDetails.name.slice(-3) === '...') {
231 argDetails.variadic = true;
232 argDetails.name = argDetails.name.slice(0, -3);
234 if (argDetails.name) {
235 self._args.push(argDetails);
242 * Register callback `fn` for the command.
248 * .description('display verbose help')
249 * .action(function() {
250 * // output help here
253 * @param {Function} fn
254 * @return {Command} for chaining
258 Command.prototype.action = function(fn) {
260 var listener = function(args, unknown) {
261 // Parse any so-far unknown options
263 unknown = unknown || [];
265 var parsed = self.parseOptions(unknown);
267 // Output help if necessary
268 outputHelpIfNecessary(self, parsed.unknown);
270 // If there are still any unknown options, then we simply
271 // die, unless someone asked for help, in which case we give it
272 // to them, and then we die.
273 if (parsed.unknown.length > 0) {
274 self.unknownOption(parsed.unknown[0]);
277 // Leftover arguments need to be pushed back. Fixes issue #56
278 if (parsed.args.length) args = parsed.args.concat(args);
280 self._args.forEach(function(arg, i) {
281 if (arg.required && null == args[i]) {
282 self.missingArgument(arg.name);
283 } else if (arg.variadic) {
284 if (i !== self._args.length - 1) {
285 self.variadicArgNotLast(arg.name);
288 args[i] = args.splice(i);
292 // Always append ourselves to the end of the arguments,
293 // to make sure we match the number of arguments the user
295 if (self._args.length) {
296 args[self._args.length] = self;
301 fn.apply(self, args);
303 var parent = this.parent || this;
304 var name = parent === this ? '*' : this._name;
305 parent.on(name, listener);
306 if (this._alias) parent.on(this._alias, listener);
311 * Define option with `flags`, `description` and optional
314 * The `flags` string should contain both the short and long flags,
315 * separated by comma, a pipe or space. The following are all valid
316 * all will output this way when `--help` is used.
324 * // simple boolean defaulting to false
325 * program.option('-p, --pepper', 'add pepper');
331 * // simple boolean defaulting to true
332 * program.option('-C, --no-cheese', 'remove cheese');
341 * // required argument
342 * program.option('-C, --chdir <path>', 'change the working directory');
348 * // optional argument
349 * program.option('-c, --cheese [type]', 'add cheese [marble]');
351 * @param {String} flags
352 * @param {String} description
353 * @param {Function|Mixed} fn or default
354 * @param {Mixed} defaultValue
355 * @return {Command} for chaining
359 Command.prototype.option = function(flags, description, fn, defaultValue) {
361 , option = new Option(flags, description)
362 , oname = option.name()
363 , name = camelcase(oname);
365 // default as 3rd arg
366 if (typeof fn != 'function') {
367 if (fn instanceof RegExp) {
369 fn = function(val, def) {
370 var m = regex.exec(val);
371 return m ? m[0] : def;
380 // preassign default value only for --no-*, [optional], or <required>
381 if (false == option.bool || option.optional || option.required) {
382 // when --no-* we make sure default is true
383 if (false == option.bool) defaultValue = true;
384 // preassign only if we have a default
385 if (undefined !== defaultValue) self[name] = defaultValue;
388 // register the option
389 this.options.push(option);
391 // when it's passed assign the value
392 // and conditionally invoke the callback
393 this.on(oname, function(val) {
395 if (null !== val && fn) val = fn(val, undefined === self[name]
399 // unassigned or bool
400 if ('boolean' == typeof self[name] || 'undefined' == typeof self[name]) {
401 // if no value, bool true, and we have a default, then use it!
403 self[name] = option.bool
404 ? defaultValue || true
409 } else if (null !== val) {
419 * Allow unknown options on the command line.
421 * @param {Boolean} arg if `true` or omitted, no error will be thrown
422 * for unknown options.
425 Command.prototype.allowUnknownOption = function(arg) {
426 this._allowUnknownOption = arguments.length === 0 || arg;
431 * Parse `argv`, settings options and invoking commands when defined.
433 * @param {Array} argv
434 * @return {Command} for chaining
438 Command.prototype.parse = function(argv) {
440 if (this.executables) this.addImplicitHelpCommand();
446 this._name = this._name || basename(argv[1], '.js');
448 // github-style sub-commands with no sub-command
449 if (this.executables && argv.length < 3 && !this.defaultExecutable) {
450 // this user needs help
455 var parsed = this.parseOptions(this.normalize(argv.slice(2)));
456 var args = this.args = parsed.args;
458 var result = this.parseArgs(this.args, parsed.unknown);
460 // executable sub-commands
461 var name = result.args[0];
462 if (this._execs[name] && typeof this._execs[name] != "function") {
463 return this.executeSubCommand(argv, args, parsed.unknown);
464 } else if (this.defaultExecutable) {
465 // use the default subcommand
466 args.unshift(name = this.defaultExecutable);
467 return this.executeSubCommand(argv, args, parsed.unknown);
474 * Execute a sub-command executable.
476 * @param {Array} argv
477 * @param {Array} args
478 * @param {Array} unknown
482 Command.prototype.executeSubCommand = function(argv, args, unknown) {
483 args = args.concat(unknown);
485 if (!args.length) this.help();
486 if ('help' == args[0] && 1 == args.length) this.help();
489 if ('help' == args[0]) {
496 // name of the subcommand, link `pm-install`
497 var bin = basename(f, '.js') + '-' + args[0];
500 // In case of globally installed, get the base dir where executable
501 // subcommand file should be located at
503 , link = readlink(f);
505 // when symbolink is relative path
506 if (link !== f && link.charAt(0) !== '/') {
507 link = path.join(dirname(f), link)
509 baseDir = dirname(link);
511 // prefer local `./<bin>` to bin in the $PATH
512 var localBin = path.join(baseDir, bin);
514 // whether bin file is a js script with explicit `.js` extension
515 var isExplicitJS = false;
516 if (exists(localBin + '.js')) {
517 bin = localBin + '.js';
519 } else if (exists(localBin)) {
523 args = args.slice(1);
526 if (process.platform !== 'win32') {
528 args.unshift(localBin);
529 // add executable arguments to spawn
530 args = (process.execArgv || []).concat(args);
532 proc = spawn('node', args, { stdio: 'inherit', customFds: [0, 1, 2] });
534 proc = spawn(bin, args, { stdio: 'inherit', customFds: [0, 1, 2] });
537 args.unshift(localBin);
538 proc = spawn(process.execPath, args, { stdio: 'inherit'});
541 proc.on('close', process.exit.bind(process));
542 proc.on('error', function(err) {
543 if (err.code == "ENOENT") {
544 console.error('\n %s(1) does not exist, try --help\n', bin);
545 } else if (err.code == "EACCES") {
546 console.error('\n %s(1) not executable. try chmod or run with root\n', bin);
551 // Store the reference to the child process
552 this.runningCommand = proc;
556 * Normalize `args`, splitting joined short flags. For example
557 * the arg "-abc" is equivalent to "-a -b -c".
558 * This also normalizes equal sign and splits "--abc=def" into "--abc def".
560 * @param {Array} args
565 Command.prototype.normalize = function(args) {
571 for (var i = 0, len = args.length; i < len; ++i) {
574 lastOpt = this.optionFor(args[i-1]);
578 // Honor option terminator
579 ret = ret.concat(args.slice(i));
581 } else if (lastOpt && lastOpt.required) {
583 } else if (arg.length > 1 && '-' == arg[0] && '-' != arg[1]) {
584 arg.slice(1).split('').forEach(function(c) {
587 } else if (/^--/.test(arg) && ~(index = arg.indexOf('='))) {
588 ret.push(arg.slice(0, index), arg.slice(index + 1));
598 * Parse command `args`.
600 * When listener(s) are available those
601 * callbacks are invoked, otherwise the "*"
602 * event is emitted and those actions are invoked.
604 * @param {Array} args
605 * @return {Command} for chaining
609 Command.prototype.parseArgs = function(args, unknown) {
614 if (this.listeners(name).length) {
615 this.emit(args.shift(), args, unknown);
617 this.emit('*', args);
620 outputHelpIfNecessary(this, unknown);
622 // If there were no args and we have unknown options,
623 // then they are extraneous and we need to error.
624 if (unknown.length > 0) {
625 this.unknownOption(unknown[0]);
633 * Return an option matching `arg` if any.
635 * @param {String} arg
640 Command.prototype.optionFor = function(arg) {
641 for (var i = 0, len = this.options.length; i < len; ++i) {
642 if (this.options[i].is(arg)) {
643 return this.options[i];
649 * Parse options from `argv` returning `argv`
650 * void of these options.
652 * @param {Array} argv
657 Command.prototype.parseOptions = function(argv) {
664 var unknownOptions = [];
667 for (var i = 0; i < len; ++i) {
670 // literal args after --
681 // find matching Option
682 option = this.optionFor(arg);
687 if (option.required) {
689 if (null == arg) return this.optionMissingArgument(option);
690 this.emit(option.name(), arg);
692 } else if (option.optional) {
694 if (null == arg || ('-' == arg[0] && '-' != arg)) {
699 this.emit(option.name(), arg);
702 this.emit(option.name());
707 // looks like an option
708 if (arg.length > 1 && '-' == arg[0]) {
709 unknownOptions.push(arg);
711 // If the next argument looks like it might be
712 // an argument for this option, we pass it on.
713 // If it isn't, then it'll simply be ignored
714 if (argv[i+1] && '-' != argv[i+1][0]) {
715 unknownOptions.push(argv[++i]);
724 return { args: args, unknown: unknownOptions };
728 * Return an object containing options as key-value pairs
733 Command.prototype.opts = function() {
735 , len = this.options.length;
737 for (var i = 0 ; i < len; i++) {
738 var key = camelcase(this.options[i].name());
739 result[key] = key === 'version' ? this._version : this[key];
745 * Argument `name` is missing.
747 * @param {String} name
751 Command.prototype.missingArgument = function(name) {
753 console.error(" error: missing required argument `%s'", name);
759 * `Option` is missing an argument, but received `flag` or nothing.
761 * @param {String} option
762 * @param {String} flag
766 Command.prototype.optionMissingArgument = function(option, flag) {
769 console.error(" error: option `%s' argument missing, got `%s'", option.flags, flag);
771 console.error(" error: option `%s' argument missing", option.flags);
778 * Unknown option `flag`.
780 * @param {String} flag
784 Command.prototype.unknownOption = function(flag) {
785 if (this._allowUnknownOption) return;
787 console.error(" error: unknown option `%s'", flag);
793 * Variadic argument with `name` is not the last argument as required.
795 * @param {String} name
799 Command.prototype.variadicArgNotLast = function(name) {
801 console.error(" error: variadic arguments must be last `%s'", name);
807 * Set the program version to `str`.
809 * This method auto-registers the "-V, --version" flag
810 * which will print the version number when passed.
812 * @param {String} str
813 * @param {String} flags
814 * @return {Command} for chaining
818 Command.prototype.version = function(str, flags) {
819 if (0 == arguments.length) return this._version;
821 flags = flags || '-V, --version';
822 this.option(flags, 'output the version number');
823 this.on('version', function() {
824 process.stdout.write(str + '\n');
831 * Set the description to `str`.
833 * @param {String} str
834 * @return {String|Command}
838 Command.prototype.description = function(str) {
839 if (0 === arguments.length) return this._description;
840 this._description = str;
845 * Set an alias for the command
847 * @param {String} alias
848 * @return {String|Command}
852 Command.prototype.alias = function(alias) {
853 if (0 == arguments.length) return this._alias;
859 * Set / get the command usage `str`.
861 * @param {String} str
862 * @return {String|Command}
866 Command.prototype.usage = function(str) {
867 var args = this._args.map(function(arg) {
868 return humanReadableArgName(arg);
871 var usage = '[options]'
872 + (this.commands.length ? ' [command]' : '')
873 + (this._args.length ? ' ' + args.join(' ') : '');
875 if (0 == arguments.length) return this._usage || usage;
882 * Get the name of the command
884 * @param {String} name
885 * @return {String|Command}
889 Command.prototype.name = function() {
894 * Return the largest option length.
900 Command.prototype.largestOptionLength = function() {
901 return this.options.reduce(function(max, option) {
902 return Math.max(max, option.flags.length);
907 * Return help for options.
913 Command.prototype.optionHelp = function() {
914 var width = this.largestOptionLength();
916 // Prepend the help information
917 return [pad('-h, --help', width) + ' ' + 'output usage information']
918 .concat(this.options.map(function(option) {
919 return pad(option.flags, width) + ' ' + option.description;
925 * Return command help documentation.
931 Command.prototype.commandHelp = function() {
932 if (!this.commands.length) return '';
934 var commands = this.commands.filter(function(cmd) {
936 }).map(function(cmd) {
937 var args = cmd._args.map(function(arg) {
938 return humanReadableArgName(arg);
943 + (cmd._alias ? '|' + cmd._alias : '')
944 + (cmd.options.length ? ' [options]' : '')
950 var width = commands.reduce(function(max, command) {
951 return Math.max(max, command[0].length);
958 , commands.map(function(cmd) {
959 var desc = cmd[1] ? ' ' + cmd[1] : '';
960 return pad(cmd[0], width) + desc;
961 }).join('\n').replace(/^/gm, ' ')
967 * Return program help documentation.
973 Command.prototype.helpInformation = function() {
975 if (this._description) {
977 ' ' + this._description
982 var cmdName = this._name;
984 cmdName = cmdName + '|' + this._alias;
988 ,' Usage: ' + cmdName + ' ' + this.usage()
993 var commandHelp = this.commandHelp();
994 if (commandHelp) cmds = [commandHelp];
999 , '' + this.optionHelp().replace(/^/gm, ' ')
1012 * Output help information for this command
1017 Command.prototype.outputHelp = function(cb) {
1019 cb = function(passthru) {
1023 process.stdout.write(cb(this.helpInformation()));
1024 this.emit('--help');
1028 * Output help information and exit.
1033 Command.prototype.help = function(cb) {
1034 this.outputHelp(cb);
1039 * Camel-case the given `flag`
1041 * @param {String} flag
1046 function camelcase(flag) {
1047 return flag.split('-').reduce(function(str, word) {
1048 return str + word[0].toUpperCase() + word.slice(1);
1053 * Pad `str` to `width`.
1055 * @param {String} str
1056 * @param {Number} width
1061 function pad(str, width) {
1062 var len = Math.max(0, width - str.length);
1063 return str + Array(len + 1).join(' ');
1067 * Output help information if necessary
1069 * @param {Command} command to output help for
1070 * @param {Array} array of options to search for -h or --help
1074 function outputHelpIfNecessary(cmd, options) {
1075 options = options || [];
1076 for (var i = 0; i < options.length; i++) {
1077 if (options[i] == '--help' || options[i] == '-h') {
1085 * Takes an argument an returns its human readable equivalent for help usage.
1087 * @param {Object} arg
1092 function humanReadableArgName(arg) {
1093 var nameOutput = arg.name + (arg.variadic === true ? '...' : '');
1096 ? '<' + nameOutput + '>'
1097 : '[' + nameOutput + ']'
1100 // for versions before node v0.8 when there weren't `fs.existsSync`
1101 function exists(file) {
1103 if (fs.statSync(file).isFile()) {