index.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847
  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);
  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]);
  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]) + '-' + args[0];
  350. // check for ./<bin> first
  351. var local = path.join(dir, bin);
  352. // run it
  353. args = args.slice(1);
  354. var proc = spawn(local, args, { stdio: 'inherit', customFds: [0, 1, 2] });
  355. proc.on('error', function(err){
  356. if (err.code == "ENOENT") {
  357. console.error('\n %s(1) does not exist, try --help\n', bin);
  358. } else if (err.code == "EACCES") {
  359. console.error('\n %s(1) not executable. try chmod or run with root\n', bin);
  360. }
  361. });
  362. this.runningCommand = proc;
  363. };
  364. /**
  365. * Normalize `args`, splitting joined short flags. For example
  366. * the arg "-abc" is equivalent to "-a -b -c".
  367. * This also normalizes equal sign and splits "--abc=def" into "--abc def".
  368. *
  369. * @param {Array} args
  370. * @return {Array}
  371. * @api private
  372. */
  373. Command.prototype.normalize = function(args){
  374. var ret = []
  375. , arg
  376. , index;
  377. for (var i = 0, len = args.length; i < len; ++i) {
  378. arg = args[i];
  379. if (arg.length > 1 && '-' == arg[0] && '-' != arg[1]) {
  380. arg.slice(1).split('').forEach(function(c){
  381. ret.push('-' + c);
  382. });
  383. } else if (/^--/.test(arg) && ~(index = arg.indexOf('='))) {
  384. ret.push(arg.slice(0, index), arg.slice(index + 1));
  385. } else {
  386. ret.push(arg);
  387. }
  388. }
  389. return ret;
  390. };
  391. /**
  392. * Parse command `args`.
  393. *
  394. * When listener(s) are available those
  395. * callbacks are invoked, otherwise the "*"
  396. * event is emitted and those actions are invoked.
  397. *
  398. * @param {Array} args
  399. * @return {Command} for chaining
  400. * @api private
  401. */
  402. Command.prototype.parseArgs = function(args, unknown){
  403. var cmds = this.commands
  404. , len = cmds.length
  405. , name;
  406. if (args.length) {
  407. name = args[0];
  408. if (this.listeners(name).length) {
  409. this.emit(args.shift(), args, unknown);
  410. } else {
  411. this.emit('*', args);
  412. }
  413. } else {
  414. outputHelpIfNecessary(this, unknown);
  415. // If there were no args and we have unknown options,
  416. // then they are extraneous and we need to error.
  417. if (unknown.length > 0) {
  418. this.unknownOption(unknown[0]);
  419. }
  420. }
  421. return this;
  422. };
  423. /**
  424. * Return an option matching `arg` if any.
  425. *
  426. * @param {String} arg
  427. * @return {Option}
  428. * @api private
  429. */
  430. Command.prototype.optionFor = function(arg){
  431. for (var i = 0, len = this.options.length; i < len; ++i) {
  432. if (this.options[i].is(arg)) {
  433. return this.options[i];
  434. }
  435. }
  436. };
  437. /**
  438. * Parse options from `argv` returning `argv`
  439. * void of these options.
  440. *
  441. * @param {Array} argv
  442. * @return {Array}
  443. * @api public
  444. */
  445. Command.prototype.parseOptions = function(argv){
  446. var args = []
  447. , len = argv.length
  448. , literal
  449. , option
  450. , arg;
  451. var unknownOptions = [];
  452. // parse options
  453. for (var i = 0; i < len; ++i) {
  454. arg = argv[i];
  455. // literal args after --
  456. if ('--' == arg) {
  457. literal = true;
  458. continue;
  459. }
  460. if (literal) {
  461. args.push(arg);
  462. continue;
  463. }
  464. // find matching Option
  465. option = this.optionFor(arg);
  466. // option is defined
  467. if (option) {
  468. // requires arg
  469. if (option.required) {
  470. arg = argv[++i];
  471. if (null == arg) return this.optionMissingArgument(option);
  472. if ('-' == arg[0] && '-' != arg) return this.optionMissingArgument(option, arg);
  473. this.emit(option.name(), arg);
  474. // optional arg
  475. } else if (option.optional) {
  476. arg = argv[i+1];
  477. if (null == arg || ('-' == arg[0] && '-' != arg)) {
  478. arg = null;
  479. } else {
  480. ++i;
  481. }
  482. this.emit(option.name(), arg);
  483. // bool
  484. } else {
  485. this.emit(option.name());
  486. }
  487. continue;
  488. }
  489. // looks like an option
  490. if (arg.length > 1 && '-' == arg[0]) {
  491. unknownOptions.push(arg);
  492. // If the next argument looks like it might be
  493. // an argument for this option, we pass it on.
  494. // If it isn't, then it'll simply be ignored
  495. if (argv[i+1] && '-' != argv[i+1][0]) {
  496. unknownOptions.push(argv[++i]);
  497. }
  498. continue;
  499. }
  500. // arg
  501. args.push(arg);
  502. }
  503. return { args: args, unknown: unknownOptions };
  504. };
  505. /**
  506. * Argument `name` is missing.
  507. *
  508. * @param {String} name
  509. * @api private
  510. */
  511. Command.prototype.missingArgument = function(name){
  512. console.error();
  513. console.error(" error: missing required argument `%s'", name);
  514. console.error();
  515. process.exit(1);
  516. };
  517. /**
  518. * `Option` is missing an argument, but received `flag` or nothing.
  519. *
  520. * @param {String} option
  521. * @param {String} flag
  522. * @api private
  523. */
  524. Command.prototype.optionMissingArgument = function(option, flag){
  525. console.error();
  526. if (flag) {
  527. console.error(" error: option `%s' argument missing, got `%s'", option.flags, flag);
  528. } else {
  529. console.error(" error: option `%s' argument missing", option.flags);
  530. }
  531. console.error();
  532. process.exit(1);
  533. };
  534. /**
  535. * Unknown option `flag`.
  536. *
  537. * @param {String} flag
  538. * @api private
  539. */
  540. Command.prototype.unknownOption = function(flag){
  541. console.error();
  542. console.error(" error: unknown option `%s'", flag);
  543. console.error();
  544. process.exit(1);
  545. };
  546. /**
  547. * Set the program version to `str`.
  548. *
  549. * This method auto-registers the "-V, --version" flag
  550. * which will print the version number when passed.
  551. *
  552. * @param {String} str
  553. * @param {String} flags
  554. * @return {Command} for chaining
  555. * @api public
  556. */
  557. Command.prototype.version = function(str, flags){
  558. if (0 == arguments.length) return this._version;
  559. this._version = str;
  560. flags = flags || '-V, --version';
  561. this.option(flags, 'output the version number');
  562. this.on('version', function(){
  563. console.log(str);
  564. process.exit(0);
  565. });
  566. return this;
  567. };
  568. /**
  569. * Set the description `str`.
  570. *
  571. * @param {String} str
  572. * @return {String|Command}
  573. * @api public
  574. */
  575. Command.prototype.description = function(str){
  576. if (0 == arguments.length) return this._description;
  577. this._description = str;
  578. return this;
  579. };
  580. /**
  581. * Set / get the command usage `str`.
  582. *
  583. * @param {String} str
  584. * @return {String|Command}
  585. * @api public
  586. */
  587. Command.prototype.usage = function(str){
  588. var args = this._args.map(function(arg){
  589. return arg.required
  590. ? '<' + arg.name + '>'
  591. : '[' + arg.name + ']';
  592. });
  593. var usage = '[options'
  594. + (this.commands.length ? '] [command' : '')
  595. + ']'
  596. + (this._args.length ? ' ' + args : '');
  597. if (0 == arguments.length) return this._usage || usage;
  598. this._usage = str;
  599. return this;
  600. };
  601. /**
  602. * Return the largest option length.
  603. *
  604. * @return {Number}
  605. * @api private
  606. */
  607. Command.prototype.largestOptionLength = function(){
  608. return this.options.reduce(function(max, option){
  609. return Math.max(max, option.flags.length);
  610. }, 0);
  611. };
  612. /**
  613. * Return help for options.
  614. *
  615. * @return {String}
  616. * @api private
  617. */
  618. Command.prototype.optionHelp = function(){
  619. var width = this.largestOptionLength();
  620. // Prepend the help information
  621. return [pad('-h, --help', width) + ' ' + 'output usage information']
  622. .concat(this.options.map(function(option){
  623. return pad(option.flags, width)
  624. + ' ' + option.description;
  625. }))
  626. .join('\n');
  627. };
  628. /**
  629. * Return command help documentation.
  630. *
  631. * @return {String}
  632. * @api private
  633. */
  634. Command.prototype.commandHelp = function(){
  635. if (!this.commands.length) return '';
  636. return [
  637. ''
  638. , ' Commands:'
  639. , ''
  640. , this.commands.map(function(cmd){
  641. var args = cmd._args.map(function(arg){
  642. return arg.required
  643. ? '<' + arg.name + '>'
  644. : '[' + arg.name + ']';
  645. }).join(' ');
  646. return pad(cmd._name
  647. + (cmd.options.length
  648. ? ' [options]'
  649. : '') + ' ' + args, 22)
  650. + (cmd.description()
  651. ? ' ' + cmd.description()
  652. : '');
  653. }).join('\n').replace(/^/gm, ' ')
  654. , ''
  655. ].join('\n');
  656. };
  657. /**
  658. * Return program help documentation.
  659. *
  660. * @return {String}
  661. * @api private
  662. */
  663. Command.prototype.helpInformation = function(){
  664. return [
  665. ''
  666. , ' Usage: ' + this._name + ' ' + this.usage()
  667. , '' + this.commandHelp()
  668. , ' Options:'
  669. , ''
  670. , '' + this.optionHelp().replace(/^/gm, ' ')
  671. , ''
  672. , ''
  673. ].join('\n');
  674. };
  675. /**
  676. * Output help information for this command
  677. *
  678. * @api public
  679. */
  680. Command.prototype.outputHelp = function(){
  681. process.stdout.write(this.helpInformation());
  682. this.emit('--help');
  683. };
  684. /**
  685. * Output help information and exit.
  686. *
  687. * @api public
  688. */
  689. Command.prototype.help = function(){
  690. this.outputHelp();
  691. process.exit();
  692. };
  693. /**
  694. * Camel-case the given `flag`
  695. *
  696. * @param {String} flag
  697. * @return {String}
  698. * @api private
  699. */
  700. function camelcase(flag) {
  701. return flag.split('-').reduce(function(str, word){
  702. return str + word[0].toUpperCase() + word.slice(1);
  703. });
  704. }
  705. /**
  706. * Pad `str` to `width`.
  707. *
  708. * @param {String} str
  709. * @param {Number} width
  710. * @return {String}
  711. * @api private
  712. */
  713. function pad(str, width) {
  714. var len = Math.max(0, width - str.length);
  715. return str + Array(len + 1).join(' ');
  716. }
  717. /**
  718. * Output help information if necessary
  719. *
  720. * @param {Command} command to output help for
  721. * @param {Array} array of options to search for -h or --help
  722. * @api private
  723. */
  724. function outputHelpIfNecessary(cmd, options) {
  725. options = options || [];
  726. for (var i = 0; i < options.length; i++) {
  727. if (options[i] == '--help' || options[i] == '-h') {
  728. cmd.outputHelp();
  729. process.exit(0);
  730. }
  731. }
  732. }