index.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852
  1. /**
  2. * Module dependencies.
  3. */
  4. var EventEmitter = require('events').EventEmitter;
  5. var spawn = require('child_process').spawn;
  6. var fs = require('fs');
  7. var exists = fs.existsSync;
  8. var path = require('path');
  9. var dirname = path.dirname;
  10. var basename = path.basename;
  11. /**
  12. * Expose the root command.
  13. */
  14. exports = module.exports = new Command;
  15. /**
  16. * Expose `Command`.
  17. */
  18. exports.Command = Command;
  19. /**
  20. * Expose `Option`.
  21. */
  22. exports.Option = Option;
  23. /**
  24. * Initialize a new `Option` with the given `flags` and `description`.
  25. *
  26. * @param {String} flags
  27. * @param {String} description
  28. * @api public
  29. */
  30. function Option(flags, description) {
  31. this.flags = flags;
  32. this.required = ~flags.indexOf('<');
  33. this.optional = ~flags.indexOf('[');
  34. this.bool = !~flags.indexOf('-no-');
  35. flags = flags.split(/[ ,|]+/);
  36. if (flags.length > 1 && !/^[[<]/.test(flags[1])) this.short = flags.shift();
  37. this.long = flags.shift();
  38. this.description = description || '';
  39. }
  40. /**
  41. * Return option name.
  42. *
  43. * @return {String}
  44. * @api private
  45. */
  46. Option.prototype.name = function(){
  47. return this.long
  48. .replace('--', '')
  49. .replace('no-', '');
  50. };
  51. /**
  52. * Check if `arg` matches the short or long flag.
  53. *
  54. * @param {String} arg
  55. * @return {Boolean}
  56. * @api private
  57. */
  58. Option.prototype.is = function(arg){
  59. return arg == this.short
  60. || arg == this.long;
  61. };
  62. /**
  63. * Initialize a new `Command`.
  64. *
  65. * @param {String} name
  66. * @api public
  67. */
  68. function Command(name) {
  69. this.commands = [];
  70. this.options = [];
  71. this._execs = [];
  72. this._args = [];
  73. this._name = name;
  74. }
  75. /**
  76. * Inherit from `EventEmitter.prototype`.
  77. */
  78. Command.prototype.__proto__ = EventEmitter.prototype;
  79. /**
  80. * Add command `name`.
  81. *
  82. * The `.action()` callback is invoked when the
  83. * command `name` is specified via __ARGV__,
  84. * and the remaining arguments are applied to the
  85. * function for access.
  86. *
  87. * When the `name` is "*" an un-matched command
  88. * will be passed as the first arg, followed by
  89. * the rest of __ARGV__ remaining.
  90. *
  91. * Examples:
  92. *
  93. * program
  94. * .version('0.0.1')
  95. * .option('-C, --chdir <path>', 'change the working directory')
  96. * .option('-c, --config <path>', 'set config path. defaults to ./deploy.conf')
  97. * .option('-T, --no-tests', 'ignore test hook')
  98. *
  99. * program
  100. * .command('setup')
  101. * .description('run remote setup commands')
  102. * .action(function(){
  103. * console.log('setup');
  104. * });
  105. *
  106. * program
  107. * .command('exec <cmd>')
  108. * .description('run the given remote command')
  109. * .action(function(cmd){
  110. * console.log('exec "%s"', cmd);
  111. * });
  112. *
  113. * program
  114. * .command('*')
  115. * .description('deploy the given env')
  116. * .action(function(env){
  117. * console.log('deploying "%s"', env);
  118. * });
  119. *
  120. * program.parse(process.argv);
  121. *
  122. * @param {String} name
  123. * @param {String} [desc]
  124. * @return {Command} the new command
  125. * @api public
  126. */
  127. Command.prototype.command = function(name, desc){
  128. var args = name.split(/ +/);
  129. var cmd = new Command(args.shift());
  130. if (desc) cmd.description(desc);
  131. if (desc) this.executables = true;
  132. if (desc) this._execs[cmd._name] = true;
  133. this.commands.push(cmd);
  134. cmd.parseExpectedArgs(args);
  135. cmd.parent = this;
  136. if (desc) return this;
  137. return cmd;
  138. };
  139. /**
  140. * Add an implicit `help [cmd]` subcommand
  141. * which invokes `--help` for the given command.
  142. *
  143. * @api private
  144. */
  145. Command.prototype.addImplicitHelpCommand = function() {
  146. this.command('help [cmd]', 'display help for [cmd]');
  147. };
  148. /**
  149. * Parse expected `args`.
  150. *
  151. * For example `["[type]"]` becomes `[{ required: false, name: 'type' }]`.
  152. *
  153. * @param {Array} args
  154. * @return {Command} for chaining
  155. * @api public
  156. */
  157. Command.prototype.parseExpectedArgs = function(args){
  158. if (!args.length) return;
  159. var self = this;
  160. args.forEach(function(arg){
  161. switch (arg[0]) {
  162. case '<':
  163. self._args.push({ required: true, name: arg.slice(1, -1) });
  164. break;
  165. case '[':
  166. self._args.push({ required: false, name: arg.slice(1, -1) });
  167. break;
  168. }
  169. });
  170. return this;
  171. };
  172. /**
  173. * Register callback `fn` for the command.
  174. *
  175. * Examples:
  176. *
  177. * program
  178. * .command('help')
  179. * .description('display verbose help')
  180. * .action(function(){
  181. * // output help here
  182. * });
  183. *
  184. * @param {Function} fn
  185. * @return {Command} for chaining
  186. * @api public
  187. */
  188. Command.prototype.action = function(fn){
  189. var self = this;
  190. this.parent.on(this._name, function(args, unknown){
  191. // Parse any so-far unknown options
  192. unknown = unknown || [];
  193. var parsed = self.parseOptions(unknown);
  194. // Output help if necessary
  195. outputHelpIfNecessary(self, parsed.unknown);
  196. // If there are still any unknown options, then we simply
  197. // die, unless someone asked for help, in which case we give it
  198. // to them, and then we die.
  199. if (parsed.unknown.length > 0) {
  200. self.unknownOption(parsed.unknown[0]);
  201. }
  202. // Leftover arguments need to be pushed back. Fixes issue #56
  203. if (parsed.args.length) args = parsed.args.concat(args);
  204. self._args.forEach(function(arg, i){
  205. if (arg.required && null == args[i]) {
  206. self.missingArgument(arg.name);
  207. }
  208. });
  209. // Always append ourselves to the end of the arguments,
  210. // to make sure we match the number of arguments the user
  211. // expects
  212. if (self._args.length) {
  213. args[self._args.length] = self;
  214. } else {
  215. args.push(self);
  216. }
  217. fn.apply(this, args);
  218. });
  219. return this;
  220. };
  221. /**
  222. * Define option with `flags`, `description` and optional
  223. * coercion `fn`.
  224. *
  225. * The `flags` string should contain both the short and long flags,
  226. * separated by comma, a pipe or space. The following are all valid
  227. * all will output this way when `--help` is used.
  228. *
  229. * "-p, --pepper"
  230. * "-p|--pepper"
  231. * "-p --pepper"
  232. *
  233. * Examples:
  234. *
  235. * // simple boolean defaulting to false
  236. * program.option('-p, --pepper', 'add pepper');
  237. *
  238. * --pepper
  239. * program.pepper
  240. * // => Boolean
  241. *
  242. * // simple boolean defaulting to false
  243. * program.option('-C, --no-cheese', 'remove cheese');
  244. *
  245. * program.cheese
  246. * // => true
  247. *
  248. * --no-cheese
  249. * program.cheese
  250. * // => true
  251. *
  252. * // required argument
  253. * program.option('-C, --chdir <path>', 'change the working directory');
  254. *
  255. * --chdir /tmp
  256. * program.chdir
  257. * // => "/tmp"
  258. *
  259. * // optional argument
  260. * program.option('-c, --cheese [type]', 'add cheese [marble]');
  261. *
  262. * @param {String} flags
  263. * @param {String} description
  264. * @param {Function|Mixed} fn or default
  265. * @param {Mixed} defaultValue
  266. * @return {Command} for chaining
  267. * @api public
  268. */
  269. Command.prototype.option = function(flags, description, fn, defaultValue){
  270. var self = this
  271. , option = new Option(flags, description)
  272. , oname = option.name()
  273. , name = camelcase(oname);
  274. // default as 3rd arg
  275. if ('function' != typeof fn) defaultValue = fn, fn = null;
  276. // preassign default value only for --no-*, [optional], or <required>
  277. if (false == option.bool || option.optional || option.required) {
  278. // when --no-* we make sure default is true
  279. if (false == option.bool) defaultValue = true;
  280. // preassign only if we have a default
  281. if (undefined !== defaultValue) self[name] = defaultValue;
  282. }
  283. // register the option
  284. this.options.push(option);
  285. // when it's passed assign the value
  286. // and conditionally invoke the callback
  287. this.on(oname, function(val){
  288. // coercion
  289. if (null !== val && fn) val = fn(val, undefined === self[name] ? defaultValue : self[name]);
  290. // unassigned or bool
  291. if ('boolean' == typeof self[name] || 'undefined' == typeof self[name]) {
  292. // if no value, bool true, and we have a default, then use it!
  293. if (null == val) {
  294. self[name] = option.bool
  295. ? defaultValue || true
  296. : false;
  297. } else {
  298. self[name] = val;
  299. }
  300. } else if (null !== val) {
  301. // reassign
  302. self[name] = val;
  303. }
  304. });
  305. return this;
  306. };
  307. /**
  308. * Parse `argv`, settings options and invoking commands when defined.
  309. *
  310. * @param {Array} argv
  311. * @return {Command} for chaining
  312. * @api public
  313. */
  314. Command.prototype.parse = function(argv){
  315. // implicit help
  316. if (this.executables) this.addImplicitHelpCommand();
  317. // store raw args
  318. this.rawArgs = argv;
  319. // guess name
  320. this._name = this._name || basename(argv[1], '.js');
  321. // process argv
  322. var parsed = this.parseOptions(this.normalize(argv.slice(2)));
  323. var args = this.args = parsed.args;
  324. var result = this.parseArgs(this.args, parsed.unknown);
  325. // executable sub-commands
  326. var name = result.args[0];
  327. if (this._execs[name]) return this.executeSubCommand(argv, args, parsed.unknown);
  328. return result;
  329. };
  330. /**
  331. * Execute a sub-command executable.
  332. *
  333. * @param {Array} argv
  334. * @param {Array} args
  335. * @param {Array} unknown
  336. * @api private
  337. */
  338. Command.prototype.executeSubCommand = function(argv, args, unknown) {
  339. args = args.concat(unknown);
  340. if (!args.length) this.help();
  341. if ('help' == args[0] && 1 == args.length) this.help();
  342. // <cmd> --help
  343. if ('help' == args[0]) {
  344. args[0] = args[1];
  345. args[1] = '--help';
  346. }
  347. // executable
  348. var dir = dirname(argv[1]);
  349. var bin = basename(argv[1], '.js') + '-' + args[0];
  350. // check for ./<bin> first
  351. var local = path.join(dir, bin);
  352. // run it
  353. args = args.slice(1);
  354. args.unshift(local);
  355. var proc = spawn('node', args, { stdio: 'inherit', customFds: [0, 1, 2] });
  356. proc.on('error', function(err){
  357. if (err.code == "ENOENT") {
  358. console.error('\n %s(1) does not exist, try --help\n', bin);
  359. } else if (err.code == "EACCES") {
  360. console.error('\n %s(1) not executable. try chmod or run with root\n', bin);
  361. }
  362. });
  363. this.runningCommand = proc;
  364. };
  365. /**
  366. * Normalize `args`, splitting joined short flags. For example
  367. * the arg "-abc" is equivalent to "-a -b -c".
  368. * This also normalizes equal sign and splits "--abc=def" into "--abc def".
  369. *
  370. * @param {Array} args
  371. * @return {Array}
  372. * @api private
  373. */
  374. Command.prototype.normalize = function(args){
  375. var ret = []
  376. , arg
  377. , lastOpt
  378. , index;
  379. for (var i = 0, len = args.length; i < len; ++i) {
  380. arg = args[i];
  381. i > 0 && (lastOpt = this.optionFor(args[i-1]));
  382. if (lastOpt && lastOpt.required) {
  383. ret.push(arg);
  384. } else if (arg.length > 1 && '-' == arg[0] && '-' != arg[1]) {
  385. arg.slice(1).split('').forEach(function(c){
  386. ret.push('-' + c);
  387. });
  388. } else if (/^--/.test(arg) && ~(index = arg.indexOf('='))) {
  389. ret.push(arg.slice(0, index), arg.slice(index + 1));
  390. } else {
  391. ret.push(arg);
  392. }
  393. }
  394. return ret;
  395. };
  396. /**
  397. * Parse command `args`.
  398. *
  399. * When listener(s) are available those
  400. * callbacks are invoked, otherwise the "*"
  401. * event is emitted and those actions are invoked.
  402. *
  403. * @param {Array} args
  404. * @return {Command} for chaining
  405. * @api private
  406. */
  407. Command.prototype.parseArgs = function(args, unknown){
  408. var cmds = this.commands
  409. , len = cmds.length
  410. , name;
  411. if (args.length) {
  412. name = args[0];
  413. if (this.listeners(name).length) {
  414. this.emit(args.shift(), args, unknown);
  415. } else {
  416. this.emit('*', args);
  417. }
  418. } else {
  419. outputHelpIfNecessary(this, unknown);
  420. // If there were no args and we have unknown options,
  421. // then they are extraneous and we need to error.
  422. if (unknown.length > 0) {
  423. this.unknownOption(unknown[0]);
  424. }
  425. }
  426. return this;
  427. };
  428. /**
  429. * Return an option matching `arg` if any.
  430. *
  431. * @param {String} arg
  432. * @return {Option}
  433. * @api private
  434. */
  435. Command.prototype.optionFor = function(arg){
  436. for (var i = 0, len = this.options.length; i < len; ++i) {
  437. if (this.options[i].is(arg)) {
  438. return this.options[i];
  439. }
  440. }
  441. };
  442. /**
  443. * Parse options from `argv` returning `argv`
  444. * void of these options.
  445. *
  446. * @param {Array} argv
  447. * @return {Array}
  448. * @api public
  449. */
  450. Command.prototype.parseOptions = function(argv){
  451. var args = []
  452. , len = argv.length
  453. , literal
  454. , option
  455. , arg;
  456. var unknownOptions = [];
  457. // parse options
  458. for (var i = 0; i < len; ++i) {
  459. arg = argv[i];
  460. // literal args after --
  461. if ('--' == arg) {
  462. literal = true;
  463. continue;
  464. }
  465. if (literal) {
  466. args.push(arg);
  467. continue;
  468. }
  469. // find matching Option
  470. option = this.optionFor(arg);
  471. // option is defined
  472. if (option) {
  473. // requires arg
  474. if (option.required) {
  475. arg = argv[++i];
  476. if (null == arg) return this.optionMissingArgument(option);
  477. this.emit(option.name(), arg);
  478. // optional arg
  479. } else if (option.optional) {
  480. arg = argv[i+1];
  481. if (null == arg || ('-' == arg[0] && '-' != arg)) {
  482. arg = null;
  483. } else {
  484. ++i;
  485. }
  486. this.emit(option.name(), arg);
  487. // bool
  488. } else {
  489. this.emit(option.name());
  490. }
  491. continue;
  492. }
  493. // looks like an option
  494. if (arg.length > 1 && '-' == arg[0]) {
  495. unknownOptions.push(arg);
  496. // If the next argument looks like it might be
  497. // an argument for this option, we pass it on.
  498. // If it isn't, then it'll simply be ignored
  499. if (argv[i+1] && '-' != argv[i+1][0]) {
  500. unknownOptions.push(argv[++i]);
  501. }
  502. continue;
  503. }
  504. // arg
  505. args.push(arg);
  506. }
  507. return { args: args, unknown: unknownOptions };
  508. };
  509. /**
  510. * Argument `name` is missing.
  511. *
  512. * @param {String} name
  513. * @api private
  514. */
  515. Command.prototype.missingArgument = function(name){
  516. console.error();
  517. console.error(" error: missing required argument `%s'", name);
  518. console.error();
  519. process.exit(1);
  520. };
  521. /**
  522. * `Option` is missing an argument, but received `flag` or nothing.
  523. *
  524. * @param {String} option
  525. * @param {String} flag
  526. * @api private
  527. */
  528. Command.prototype.optionMissingArgument = function(option, flag){
  529. console.error();
  530. if (flag) {
  531. console.error(" error: option `%s' argument missing, got `%s'", option.flags, flag);
  532. } else {
  533. console.error(" error: option `%s' argument missing", option.flags);
  534. }
  535. console.error();
  536. process.exit(1);
  537. };
  538. /**
  539. * Unknown option `flag`.
  540. *
  541. * @param {String} flag
  542. * @api private
  543. */
  544. Command.prototype.unknownOption = function(flag){
  545. console.error();
  546. console.error(" error: unknown option `%s'", flag);
  547. console.error();
  548. process.exit(1);
  549. };
  550. /**
  551. * Set the program version to `str`.
  552. *
  553. * This method auto-registers the "-V, --version" flag
  554. * which will print the version number when passed.
  555. *
  556. * @param {String} str
  557. * @param {String} flags
  558. * @return {Command} for chaining
  559. * @api public
  560. */
  561. Command.prototype.version = function(str, flags){
  562. if (0 == arguments.length) return this._version;
  563. this._version = str;
  564. flags = flags || '-V, --version';
  565. this.option(flags, 'output the version number');
  566. this.on('version', function(){
  567. console.log(str);
  568. process.exit(0);
  569. });
  570. return this;
  571. };
  572. /**
  573. * Set the description `str`.
  574. *
  575. * @param {String} str
  576. * @return {String|Command}
  577. * @api public
  578. */
  579. Command.prototype.description = function(str){
  580. if (0 == arguments.length) return this._description;
  581. this._description = str;
  582. return this;
  583. };
  584. /**
  585. * Set / get the command usage `str`.
  586. *
  587. * @param {String} str
  588. * @return {String|Command}
  589. * @api public
  590. */
  591. Command.prototype.usage = function(str){
  592. var args = this._args.map(function(arg){
  593. return arg.required
  594. ? '<' + arg.name + '>'
  595. : '[' + arg.name + ']';
  596. });
  597. var usage = '[options'
  598. + (this.commands.length ? '] [command' : '')
  599. + ']'
  600. + (this._args.length ? ' ' + args : '');
  601. if (0 == arguments.length) return this._usage || usage;
  602. this._usage = str;
  603. return this;
  604. };
  605. /**
  606. * Return the largest option length.
  607. *
  608. * @return {Number}
  609. * @api private
  610. */
  611. Command.prototype.largestOptionLength = function(){
  612. return this.options.reduce(function(max, option){
  613. return Math.max(max, option.flags.length);
  614. }, 0);
  615. };
  616. /**
  617. * Return help for options.
  618. *
  619. * @return {String}
  620. * @api private
  621. */
  622. Command.prototype.optionHelp = function(){
  623. var width = this.largestOptionLength();
  624. // Prepend the help information
  625. return [pad('-h, --help', width) + ' ' + 'output usage information']
  626. .concat(this.options.map(function(option){
  627. return pad(option.flags, width)
  628. + ' ' + option.description;
  629. }))
  630. .join('\n');
  631. };
  632. /**
  633. * Return command help documentation.
  634. *
  635. * @return {String}
  636. * @api private
  637. */
  638. Command.prototype.commandHelp = function(){
  639. if (!this.commands.length) return '';
  640. return [
  641. ''
  642. , ' Commands:'
  643. , ''
  644. , this.commands.map(function(cmd){
  645. var args = cmd._args.map(function(arg){
  646. return arg.required
  647. ? '<' + arg.name + '>'
  648. : '[' + arg.name + ']';
  649. }).join(' ');
  650. return pad(cmd._name
  651. + (cmd.options.length
  652. ? ' [options]'
  653. : '') + ' ' + args, 22)
  654. + (cmd.description()
  655. ? ' ' + cmd.description()
  656. : '');
  657. }).join('\n').replace(/^/gm, ' ')
  658. , ''
  659. ].join('\n');
  660. };
  661. /**
  662. * Return program help documentation.
  663. *
  664. * @return {String}
  665. * @api private
  666. */
  667. Command.prototype.helpInformation = function(){
  668. return [
  669. ''
  670. , ' Usage: ' + this._name + ' ' + this.usage()
  671. , '' + this.commandHelp()
  672. , ' Options:'
  673. , ''
  674. , '' + this.optionHelp().replace(/^/gm, ' ')
  675. , ''
  676. , ''
  677. ].join('\n');
  678. };
  679. /**
  680. * Output help information for this command
  681. *
  682. * @api public
  683. */
  684. Command.prototype.outputHelp = function(){
  685. process.stdout.write(this.helpInformation());
  686. this.emit('--help');
  687. };
  688. /**
  689. * Output help information and exit.
  690. *
  691. * @api public
  692. */
  693. Command.prototype.help = function(){
  694. this.outputHelp();
  695. process.exit();
  696. };
  697. /**
  698. * Camel-case the given `flag`
  699. *
  700. * @param {String} flag
  701. * @return {String}
  702. * @api private
  703. */
  704. function camelcase(flag) {
  705. return flag.split('-').reduce(function(str, word){
  706. return str + word[0].toUpperCase() + word.slice(1);
  707. });
  708. }
  709. /**
  710. * Pad `str` to `width`.
  711. *
  712. * @param {String} str
  713. * @param {Number} width
  714. * @return {String}
  715. * @api private
  716. */
  717. function pad(str, width) {
  718. var len = Math.max(0, width - str.length);
  719. return str + Array(len + 1).join(' ');
  720. }
  721. /**
  722. * Output help information if necessary
  723. *
  724. * @param {Command} command to output help for
  725. * @param {Array} array of options to search for -h or --help
  726. * @api private
  727. */
  728. function outputHelpIfNecessary(cmd, options) {
  729. options = options || [];
  730. for (var i = 0; i < options.length; i++) {
  731. if (options[i] == '--help' || options[i] == '-h') {
  732. cmd.outputHelp();
  733. process.exit(0);
  734. }
  735. }
  736. }