cli.js 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112
  1. /**
  2. * Copyright (c) 2010 Chris O'Hara <cohara87@gmail.com>
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining
  5. * a copy of this software and associated documentation files (the
  6. * "Software"), to deal in the Software without restriction, including
  7. * without limitation the rights to use, copy, modify, merge, publish,
  8. * distribute, sublicense, and/or sell copies of the Software, and to
  9. * permit persons to whom the Software is furnished to do so, subject to
  10. * the following conditions:
  11. *
  12. * The above copyright notice and this permission notice shall be
  13. * included in all copies or substantial portions of the Software.
  14. *
  15. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  16. * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  17. * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  18. * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  19. * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  20. * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  21. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  22. */
  23. //Note: cli includes kof/node-natives and creationix/stack. I couldn't find
  24. //license information for either - contact me if you want your license added
  25. var cli = exports,
  26. argv, curr_opt, curr_val, full_opt, is_long,
  27. short_tags = [], opt_list, parsed = {},
  28. usage, argv_parsed, command_list, commands,
  29. show_debug;
  30. cli.app = null;
  31. cli.version = null;
  32. cli.argv = [];
  33. cli.argc = 0;
  34. cli.options = {};
  35. cli.args = [];
  36. cli.command = null;
  37. cli.width = 70;
  38. cli.option_width = 25;
  39. /**
  40. * Bind kof's node-natives (https://github.com/kof/node-natives) to `cli.native`
  41. *
  42. * Rather than requiring node natives (e.g. var fs = require('fs')), all
  43. * native modules can be accessed like `cli.native.fs`
  44. */
  45. cli.native = {};
  46. var define_native = function (module) {
  47. Object.defineProperty(cli.native, module, {
  48. enumerable: true,
  49. configurable: true,
  50. get: function() {
  51. delete cli.native[module];
  52. return (cli.native[module] = require(module));
  53. }
  54. });
  55. };
  56. var natives = process.binding('natives');
  57. for (var module in natives) {
  58. define_native(module);
  59. }
  60. cli.output = console.log;
  61. cli.exit = require('exit');
  62. cli.no_color = false;
  63. if (process.env.NODE_DISABLE_COLORS || process.env.TERM === 'dumb') {
  64. cli.no_color = true;
  65. }
  66. /**
  67. * Define plugins. Plugins can be enabled and disabled by calling:
  68. *
  69. * `cli.enable(plugin1, [plugin2, ...])`
  70. * `cli.disable(plugin1, [plugin2, ...])`
  71. *
  72. * Methods are chainable - `cli.enable(plugin).disable(plugin2)`.
  73. *
  74. * The 'help' plugin is enabled by default.
  75. */
  76. var enable = {
  77. help: true, //Adds -h, --help
  78. version: false, //Adds -v,--version => gets version by parsing a nearby package.json
  79. status: false, //Adds -k,--no-color & --debug => display plain status messages /display debug messages
  80. timeout: false, //Adds -t,--timeout N => timeout the process after N seconds
  81. catchall: false, //Adds -c,--catch => catch and output uncaughtExceptions
  82. glob: false //Adds glob matching => use cli.glob(arg)
  83. }
  84. cli.enable = function (/*plugins*/) {
  85. Array.prototype.slice.call(arguments).forEach(function (plugin) {
  86. switch (plugin) {
  87. case 'catchall':
  88. process.on('uncaughtException', function (err) {
  89. cli.error('Uncaught exception: ' + (err.msg || err));
  90. });
  91. break;
  92. case 'help': case 'version': case 'status':
  93. case 'autocomplete': case 'timeout':
  94. //Just add switches.
  95. break;
  96. case 'glob':
  97. cli.glob = require('glob');
  98. break;
  99. default:
  100. cli.fatal('Unknown plugin "' + plugin + '"');
  101. break;
  102. }
  103. enable[plugin] = true;
  104. });
  105. return cli;
  106. }
  107. cli.disable = function (/*plugins*/) {
  108. Array.prototype.slice.call(arguments).forEach(function (plugin) {
  109. if (enable[plugin]) {
  110. enable[plugin] = false;
  111. }
  112. });
  113. return cli;
  114. }
  115. /**
  116. * Sets argv (default is process.argv).
  117. *
  118. * @param {Array|String} argv
  119. * @param {Boolean} keep_arg0 (optional - default is false)
  120. * @api public
  121. */
  122. cli.setArgv = function (arr, keep_arg0) {
  123. if (typeof arr == 'string') {
  124. arr = arr.split(' ');
  125. } else {
  126. arr = arr.slice();
  127. }
  128. cli.app = arr.shift();
  129. // Strip off argv[0] if it's a node binary
  130. // So this is still broken and will break if you are calling node through a
  131. // symlink, unless you are lucky enough to have it as 'node' literal. Latter
  132. // is a hack, but resolving abspaths/symlinks is an unportable can of worms.
  133. if (!keep_arg0 && (['node', 'node.exe'].indexOf(cli.native.path.basename(cli.app)) !== -1
  134. || cli.native.path.basename(process.execPath) === cli.app
  135. || process.execPath === cli.app)) {
  136. cli.app = arr.shift();
  137. }
  138. cli.app = cli.native.path.basename(cli.app);
  139. argv_parsed = false;
  140. cli.args = cli.argv = argv = arr;
  141. cli.argc = argv.length;
  142. cli.options = {};
  143. cli.command = null;
  144. };
  145. cli.setArgv(process.argv);
  146. /**
  147. * Returns the next opt, or false if no opts are found.
  148. *
  149. * @return {String} opt
  150. * @api public
  151. */
  152. cli.next = function () {
  153. if (!argv_parsed) {
  154. cli.args = [];
  155. argv_parsed = true;
  156. }
  157. curr_val = null;
  158. //If we're currently in a group of short opts (e.g. -abc), return the next opt
  159. if (short_tags.length) {
  160. curr_opt = short_tags.shift();
  161. full_opt = '-' + curr_opt;
  162. return curr_opt;
  163. }
  164. if (!argv.length) {
  165. return false;
  166. }
  167. curr_opt = argv.shift();
  168. //If an escape sequence is found (- or --), subsequent opts are ignored
  169. if (curr_opt === '-' || curr_opt === '--') {
  170. while (argv.length) {
  171. cli.args.push(argv.shift());
  172. }
  173. return false;
  174. }
  175. //If the next element in argv isn't an opt, add it to the list of args
  176. if (curr_opt[0] !== '-') {
  177. cli.args.push(curr_opt);
  178. return cli.next();
  179. } else {
  180. //Check if the opt is short/long
  181. is_long = curr_opt[1] === '-';
  182. curr_opt = curr_opt.substr(is_long ? 2 : 1);
  183. }
  184. //Accept grouped short opts, e.g. -abc => -a -b -c
  185. if (!is_long && curr_opt.length > 1) {
  186. short_tags = curr_opt.split('');
  187. return cli.next();
  188. }
  189. var eq, len;
  190. //Check if the long opt is in the form --option=VALUE
  191. if (is_long && (eq = curr_opt.indexOf('=')) >= 0) {
  192. curr_val = curr_opt.substr(eq + 1);
  193. curr_opt = curr_opt.substr(0, eq);
  194. len = curr_val.length;
  195. //Allow values to be quoted
  196. if ((curr_val[0] === '"' && curr_val[len - 1] === '"') ||
  197. (curr_val[0] === "'" && curr_val[len - 1] === "'"))
  198. {
  199. curr_val = curr_val.substr(1, len-2);
  200. }
  201. if (curr_val.match(/^[0-9]+$/)) {
  202. curr_val = parseInt(curr_val, 10);
  203. }
  204. }
  205. //Save the opt representation for later
  206. full_opt = (is_long ? '--' : '-') + curr_opt;
  207. return curr_opt;
  208. };
  209. /**
  210. * Parses command line opts.
  211. *
  212. * `opts` must be an object with opts defined like:
  213. * long_tag: [short_tag, description, value_type, default_value];
  214. *
  215. * `commands` is an optional array or object for apps that are of the form
  216. * my_app [OPTIONS] <command> [ARGS]
  217. * The command list is output with usage information + there is bundled
  218. * support for auto-completion, etc.
  219. *
  220. * See README.md for more information.
  221. *
  222. * @param {Object} opts
  223. * @param {Object} commands (optional)
  224. * @return {Object} opts (parsed)
  225. * @api public
  226. */
  227. cli.parse = function (opts, command_def) {
  228. var default_val, i, o, parsed = cli.options, seen,
  229. catch_all = !opts;
  230. opt_list = opts || {};
  231. commands = command_def;
  232. command_list = commands || [];
  233. if (commands && !Array.isArray(commands)) {
  234. command_list = Object.keys(commands);
  235. }
  236. while ((o = cli.next())) {
  237. seen = false;
  238. for (var opt in opt_list) {
  239. if (!(opt_list[opt] instanceof Array)) {
  240. continue;
  241. }
  242. if (!opt_list[opt][0]) {
  243. opt_list[opt][0] = opt;
  244. }
  245. if (o === opt || o === opt_list[opt][0]) {
  246. seen = true;
  247. if (opt_list[opt].length === 2) {
  248. parsed[opt] = true;
  249. break;
  250. }
  251. default_val = null;
  252. if (opt_list[opt].length === 4) {
  253. default_val = opt_list[opt][3];
  254. }
  255. if (opt_list[opt][2] instanceof Array) {
  256. for (i = 0, l = opt_list[opt][2].length; i < l; i++) {
  257. if (typeof opt_list[opt][2][i] === 'number') {
  258. opt_list[opt][2][i] += '';
  259. }
  260. }
  261. parsed[opt] = cli.getArrayValue(opt_list[opt][2], is_long ? null : default_val);
  262. break;
  263. }
  264. if (opt_list[opt][2].toLowerCase) {
  265. opt_list[opt][2] = opt_list[opt][2].toLowerCase();
  266. }
  267. switch (opt_list[opt][2]) {
  268. case 'string': case 1: case true:
  269. parsed[opt] = cli.getValue(default_val);
  270. break;
  271. case 'int': case 'number': case 'num':
  272. case 'time': case 'seconds': case 'secs': case 'minutes': case 'mins':
  273. case 'x': case 'n':
  274. parsed[opt] = cli.getInt(default_val);
  275. break;
  276. case 'date': case 'datetime': case 'date_time':
  277. parsed[opt] = cli.getDate(default_val);
  278. break;
  279. case 'float': case 'decimal':
  280. parsed[opt] = cli.getFloat(default_val);
  281. break;
  282. case 'path': case 'file': case 'directory': case 'dir':
  283. parsed[opt] = cli.getPath(default_val, opt_list[opt][2]);
  284. break;
  285. case 'email':
  286. parsed[opt] = cli.getEmail(default_val);
  287. break;
  288. case 'url': case 'uri': case 'domain': case 'host':
  289. parsed[opt] = cli.getUrl(default_val, opt_list[opt][2]);
  290. break;
  291. case 'ip':
  292. parsed[opt] = cli.getIp(default_val);
  293. break;
  294. case 'bool': case 'boolean': case 'on':
  295. parsed[opt] = true;
  296. break;
  297. case 'false': case 'off': case false: case 0:
  298. parsed[opt] = false;
  299. break;
  300. default:
  301. cli.fatal('Unknown opt type "' + opt_list[opt][2] + '"');
  302. }
  303. break;
  304. }
  305. }
  306. if (!seen) {
  307. if (enable.help && (o === 'h' || o === 'help')) {
  308. cli.getUsage();
  309. } else if (enable.version && (o === 'v' || o === 'version')) {
  310. if (cli.version == null) {
  311. cli.parsePackageJson();
  312. }
  313. console.error(cli.app + ' v' + cli.version);
  314. cli.exit();
  315. break;
  316. } else if (enable.catchall && (o === 'c' || o === 'catch')) {
  317. continue;
  318. } else if (enable.status && (o === 'k' || o === 'no-color')) {
  319. cli.no_color = (o === 'k' || o === 'no-color');
  320. continue;
  321. } else if (enable.status && (o === 'debug')) {
  322. show_debug = o === 'debug';
  323. continue;
  324. } else if (enable.timeout && (o === 't' || o === 'timeout')) {
  325. var secs = cli.getInt();
  326. setTimeout(function () {
  327. cli.fatal('Process timed out after ' + secs + 's');
  328. }, secs * 1000);
  329. continue;
  330. } else if (catch_all) {
  331. parsed[o] = curr_val || true;
  332. continue;
  333. }
  334. cli.fatal('Unknown option ' + full_opt);
  335. }
  336. }
  337. //Fill the remaining options with their default value or null
  338. for (var opt in opt_list) {
  339. default_val = opt_list[opt].length === 4 ? opt_list[opt][3] : null;
  340. if (!(opt_list[opt] instanceof Array)) {
  341. parsed[opt] = opt_list[opt];
  342. continue;
  343. } else if (typeof parsed[opt] === 'undefined') {
  344. parsed[opt] = default_val;
  345. }
  346. }
  347. if (command_list.length) {
  348. if (cli.args.length === 0) {
  349. if (enable.help) {
  350. cli.getUsage();
  351. } else {
  352. cli.fatal('A command is required (' + command_list.join(', ') + ').');
  353. }
  354. return cli.exit(1);
  355. } else {
  356. cli.command = cli.autocompleteCommand(cli.args.shift());
  357. }
  358. }
  359. cli.argc = cli.args.length;
  360. return parsed;
  361. };
  362. /**
  363. * Helper method for matching a command from the command list.
  364. *
  365. * @param {String} command
  366. * @return {String} full_command
  367. * @api public
  368. */
  369. cli.autocompleteCommand = function (command) {
  370. var list;
  371. if (!(command_list instanceof Array)) {
  372. list = Object.keys(command_list);
  373. } else {
  374. list = command_list;
  375. }
  376. var i, j = 0, c = command.length, tmp_list;
  377. if (list.length === 0 || list.indexOf(command) !== -1) {
  378. return command;
  379. }
  380. for (i = 0; i < c; i++) {
  381. tmp_list = [];
  382. l = list.length;
  383. if (l <= 1) break;
  384. for (j = 0; j < l; j++)
  385. if (list[j].length >= i && list[j][i] === command[i])
  386. tmp_list.push(list[j]);
  387. list = tmp_list;
  388. }
  389. l = list.length;
  390. if (l === 1) {
  391. return list[0];
  392. } else if (l === 0) {
  393. cli.fatal('Unknown command "' + command + '"' + (enable.help ? '. Please see --help for more information' : ''));
  394. } else {
  395. list.sort();
  396. cli.fatal('The command "' + command + '" is ambiguous and could mean "' + list.join('", "') + '"');
  397. }
  398. };
  399. /**
  400. * Adds methods to output styled status messages to stderr.
  401. *
  402. * Added methods are cli.info(msg), cli.error(msg), cli.ok(msg), and
  403. * cli.debug(msg).
  404. *
  405. * To control status messages, use the 'status' plugin
  406. * 1) debug() messages are hidden by default. Display them with
  407. * the --debug opt.
  408. * 2) to hide all status messages, use the -s or --silent opt.
  409. *
  410. * @api private
  411. */
  412. cli.status = function (msg, type) {
  413. var pre;
  414. switch (type) {
  415. case 'info':
  416. pre = cli.no_color ? 'INFO:' : '\x1B[33mINFO\x1B[0m:';
  417. break;
  418. case 'debug':
  419. pre = cli.no_color ? 'DEBUG:' : '\x1B[36mDEBUG\x1B[0m:';
  420. break;
  421. case 'error':
  422. case 'fatal':
  423. pre = cli.no_color ? 'ERROR:' : '\x1B[31mERROR\x1B[0m:';
  424. break;
  425. case 'ok':
  426. pre = cli.no_color ? 'OK:' : '\x1B[32mOK\x1B[0m:';
  427. break;
  428. }
  429. msg = pre + ' ' + msg;
  430. if (type === 'fatal') {
  431. console.error(msg);
  432. return cli.exit(1);
  433. }
  434. if (enable.status && !show_debug && type === 'debug') {
  435. return;
  436. }
  437. console.error(msg);
  438. };
  439. ['info','error','ok','debug','fatal'].forEach(function (type) {
  440. cli[type] = function (msg) {
  441. cli.status(msg, type);
  442. };
  443. });
  444. /**
  445. * Sets the app name and version.
  446. *
  447. * Usage:
  448. * setApp('myapp', '0.1.0');
  449. * setApp('./package.json'); //Pull name/version from package.json
  450. *
  451. * @param {String} name
  452. * @return cli (for chaining)
  453. * @api public
  454. */
  455. cli.setApp = function (name, version) {
  456. if (name.indexOf('package.json') !== -1) {
  457. cli.parsePackageJson(name);
  458. } else {
  459. cli.app = name;
  460. cli.version = version;
  461. }
  462. return cli;
  463. };
  464. /**
  465. * Parses the version number from package.json. If no path is specified, cli
  466. * will attempt to locate a package.json in ./, ../ or ../../
  467. *
  468. * @param {String} path (optional)
  469. * @api public
  470. */
  471. cli.parsePackageJson = function (path) {
  472. var parse_packagejson = function (path) {
  473. var packagejson = JSON.parse(cli.native.fs.readFileSync(path, 'utf8'));
  474. cli.version = packagejson.version;
  475. cli.app = packagejson.name;
  476. };
  477. var try_all = function (arr, func, err) {
  478. for (var i = 0, l = arr.length; i < l; i++) {
  479. try {
  480. func(arr[i]);
  481. return;
  482. } catch (e) {
  483. if (i === l-1) {
  484. cli.fatal(err);
  485. }
  486. }
  487. }
  488. };
  489. try {
  490. if (path) {
  491. return parse_packagejson(path);
  492. }
  493. try_all([
  494. __dirname + '/package.json',
  495. __dirname + '/../package.json',
  496. __dirname + '/../../package.json'
  497. ], parse_packagejson);
  498. } catch (e) {
  499. cli.fatal('Could not detect ' + cli.app + ' version');
  500. }
  501. };
  502. /**
  503. * Sets the usage string - default is `app [OPTIONS] [ARGS]`.
  504. *
  505. * @param {String} u
  506. * @return cli (for chaining)
  507. * @api public
  508. */
  509. cli.setUsage = function (u) {
  510. usage = u;
  511. return cli;
  512. };
  513. var pad = function (str, len) {
  514. if (typeof len === 'undefined') {
  515. len = str;
  516. str = '';
  517. }
  518. if (str.length < len) {
  519. len -= str.length;
  520. while (len--) str += ' ';
  521. }
  522. return str;
  523. };
  524. /**
  525. * Automatically build usage information from the opts list. If the help
  526. * plugin is enabled (default), this info is displayed with -h, --help.
  527. *
  528. * @api public
  529. */
  530. cli.getUsage = function (code) {
  531. var short, desc, optional, line, seen_opts = [],
  532. switch_pad = cli.option_width;
  533. var trunc_desc = function (pref, desc, len) {
  534. var pref_len = pref.length,
  535. desc_len = cli.width - pref_len,
  536. truncated = '';
  537. if (desc.length <= desc_len) {
  538. return desc;
  539. }
  540. var desc_words = (desc+'').split(' '), chars = 0, word;
  541. while (desc_words.length) {
  542. truncated += (word = desc_words.shift()) + ' ';
  543. chars += word.length;
  544. if (desc_words.length && chars + desc_words[0].length > desc_len) {
  545. truncated += '\n' + pad(pref_len);
  546. chars = 0;
  547. }
  548. }
  549. return truncated;
  550. };
  551. usage = usage || cli.app + ' [OPTIONS]' + (command_list.length ? ' <command>' : '') + ' [ARGS]';
  552. if (cli.no_color) {
  553. console.error('Usage:\n ' + usage);
  554. console.error('Options: ');
  555. } else {
  556. console.error('\x1b[1mUsage\x1b[0m:\n ' + usage);
  557. console.error('\n\x1b[1mOptions\x1b[0m: ');
  558. }
  559. for (var opt in opt_list) {
  560. if (opt.length === 1) {
  561. long = opt_list[opt][0];
  562. short = opt;
  563. } else {
  564. long = opt;
  565. short = opt_list[opt][0];
  566. }
  567. //Parse opt_list
  568. desc = opt_list[opt][1].trim();
  569. type = opt_list[opt].length >= 3 ? opt_list[opt][2] : null;
  570. optional = opt_list[opt].length === 4 ? opt_list[opt][3] : null;
  571. //Build usage line
  572. if (short === long) {
  573. if (short.length === 1) {
  574. line = ' -' + short;
  575. } else {
  576. line = ' --' + long;
  577. }
  578. } else if (short) {
  579. line = ' -' + short + ', --' + long;
  580. } else {
  581. line = ' --' + long;
  582. }
  583. line += ' ';
  584. if (type) {
  585. if (type instanceof Array) {
  586. desc += '. VALUE must be either [' + type.join('|') + ']';
  587. type = 'VALUE';
  588. }
  589. if (type === true || type === 1) {
  590. type = long.toUpperCase();
  591. }
  592. type = type.toUpperCase();
  593. if (type === 'FLOAT' || type === 'INT') {
  594. type = 'NUMBER';
  595. }
  596. line += optional ? '[' + type + ']' : type;
  597. }
  598. line = pad(line, switch_pad);
  599. line += trunc_desc(line, desc);
  600. line += optional ? ' (Default is ' + optional + ')' : '';
  601. console.error(line.replace('%s', '%\0s'));
  602. seen_opts.push(short);
  603. seen_opts.push(long);
  604. }
  605. if (enable.timeout && seen_opts.indexOf('t') === -1 && seen_opts.indexOf('timeout') === -1) {
  606. console.error(pad(' -t, --timeout N', switch_pad) + 'Exit if the process takes longer than N seconds');
  607. }
  608. if (enable.status) {
  609. if (seen_opts.indexOf('k') === -1 && seen_opts.indexOf('no-color') === -1) {
  610. console.error(pad(' -k, --no-color', switch_pad) + 'Omit color from output');
  611. }
  612. if (seen_opts.indexOf('debug') === -1) {
  613. console.error(pad(' --debug', switch_pad) + 'Show debug information');
  614. }
  615. }
  616. if (enable.catchall && seen_opts.indexOf('c') === -1 && seen_opts.indexOf('catch') === -1) {
  617. console.error(pad(' -c, --catch', switch_pad) + 'Catch unanticipated errors');
  618. }
  619. if (enable.version && seen_opts.indexOf('v') === -1 && seen_opts.indexOf('version') === -1) {
  620. console.error(pad(' -v, --version', switch_pad) + 'Display the current version');
  621. }
  622. if (enable.help && seen_opts.indexOf('h') === -1 && seen_opts.indexOf('help') === -1) {
  623. console.error(pad(' -h, --help', switch_pad) + 'Display help and usage details');
  624. }
  625. if (command_list.length) {
  626. console.error('\n\x1b[1mCommands\x1b[0m: ');
  627. if (!Array.isArray(commands)) {
  628. for (var c in commands) {
  629. line = ' ' + pad(c, switch_pad - 2);
  630. line += trunc_desc(line, commands[c]);
  631. console.error(line);
  632. }
  633. } else {
  634. command_list.sort();
  635. console.error(' ' + trunc_desc(' ', command_list.join(', ')));
  636. }
  637. }
  638. return cli.exit(code);
  639. };
  640. /**
  641. * Generates an error message when an opt is incorrectly used.
  642. *
  643. * @param {String} expects (e.g. 'a value')
  644. * @param {String} type (e.g. 'VALUE')
  645. * @api public
  646. */
  647. cli.getOptError = function (expects, type) {
  648. var err = full_opt + ' expects ' + expects
  649. + '. Use `' + cli.app + ' ' + full_opt + (is_long ? '=' : ' ') + type + '`';
  650. return err;
  651. };
  652. /**
  653. * Gets the next opt value and validates it with an optional validation
  654. * function. If validation fails or no value can be obtained, this method
  655. * will return the default value (if specified) or exit with err_msg.
  656. *
  657. * @param {String} default_val
  658. * @param {Function} validate_func
  659. * @param {String} err_msg
  660. * @api public
  661. */
  662. cli.getValue = function (default_val, validate_func, err_msg) {
  663. err_msg = err_msg || cli.getOptError('a value', 'VALUE');
  664. var value;
  665. try {
  666. if (curr_val) {
  667. if (validate_func) {
  668. curr_val = validate_func(curr_val);
  669. }
  670. return curr_val;
  671. }
  672. //Grouped short opts aren't allowed to have values
  673. if (short_tags.length) {
  674. throw 'Short tags';
  675. }
  676. //If there's no args left or the next arg is an opt, return the
  677. //default value (if specified) - otherwise fail
  678. if (!argv.length || (argv[0].length === 1 && argv[0][0] === '-')) {
  679. throw 'No value';
  680. }
  681. value = argv.shift();
  682. if (value.match(/^[0-9]+$/)) {
  683. value = parseInt(value, 10);
  684. }
  685. //Run the value through a validation/transformation function if specified
  686. if (validate_func) {
  687. value = validate_func(value);
  688. }
  689. } catch (e) {
  690. //The value didn't pass the validation/transformation. Unshift the value and
  691. //return the default value (if specified)
  692. if (value) {
  693. argv.unshift(value);
  694. }
  695. return default_val != null ? default_val : cli.fatal(err_msg);
  696. }
  697. return value;
  698. };
  699. cli.getInt = function (default_val) {
  700. return cli.getValue(default_val, function (value) {
  701. if (typeof value === 'number') return value;
  702. if (!value.match(/^(?:-?(?:0|[1-9][0-9]*))$/)) {
  703. throw 'Invalid int';
  704. }
  705. return parseInt(value);
  706. }, cli.getOptError('a number', 'NUMBER'));
  707. }
  708. cli.getDate = function (default_val) {
  709. return cli.getValue(default_val, function (value) {
  710. if (cli.toType(value) === 'date') return value;
  711. value = new Date(value);
  712. if ( ! value.getTime() ) {
  713. throw value.toString();
  714. }
  715. return value;
  716. }, cli.getOptError('a date', 'DATE'));
  717. }
  718. cli.getFloat = function (default_val) {
  719. return cli.getValue(default_val, function (value) {
  720. if (!value.match(/^(?:-?(?:0|[1-9][0-9]*))?(?:\.[0-9]*)?$/)) {
  721. throw 'Invalid float';
  722. }
  723. return parseFloat(value, 10);
  724. }, cli.getOptError('a number', 'NUMBER'));
  725. }
  726. cli.getUrl = function (default_val, identifier) {
  727. identifier = identifier || 'url';
  728. return cli.getValue(default_val, function (value) {
  729. if (!value.match(/^(?:(?:ht|f)tp(?:s?)\:\/\/|~\/|\/)?(?:\w+:\w+@)?((?:(?:[-\w\d{1-3}]+\.)+(?:com|org|net|gov|mil|biz|info|mobi|name|aero|jobs|edu|co\.uk|ac\.uk|it|fr|tv|museum|asia|local|travel|[a-z]{2})?)|((\b25[0-5]\b|\b[2][0-4][0-9]\b|\b[0-1]?[0-9]?[0-9]\b)(\.(\b25[0-5]\b|\b[2][0-4][0-9]\b|\b[0-1]?[0-9]?[0-9]\b)){3}))(?::[\d]{1,5})?(?:(?:(?:\/(?:[-\w~!$+|.,=]|%[a-f\d]{2})+)+|\/)+|\?|#)?(?:(?:\?(?:[-\w~!$+|.,*:]|%[a-f\d{2}])+=?(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)(?:&(?:[-\w~!$+|.,*:]|%[a-f\d{2}])+=?(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)*)*(?:#(?:[-\w~!$ |\/.,*:;=]|%[a-f\d]{2})*)?$/i)) {
  730. throw 'Invalid URL';
  731. }
  732. return value;
  733. }, cli.getOptError('a ' + identifier, identifier.toUpperCase()));
  734. }
  735. cli.getEmail = function (default_val) {
  736. return cli.getValue(default_val, function (value) {
  737. if (!value.match(/^(?:[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+\.)*[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+@(?:(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!\.)){0,61}[a-zA-Z0-9]?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!$)){0,61}[a-zA-Z0-9]?)|(?:\[(?:(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\]))$/)) {
  738. throw 'Invalid email';
  739. }
  740. return value;
  741. }, cli.getOptError('an email', 'EMAIL'));
  742. }
  743. cli.getIp = function (default_val) {
  744. return cli.getValue(default_val, function (value) {
  745. if (!value.match(/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/)) {
  746. throw 'Invalid IP';
  747. }
  748. return value;
  749. }, cli.getOptError('an IP', 'IP'));
  750. }
  751. cli.getPath = function (default_val, identifier) {
  752. identifier = identifier || 'path';
  753. return cli.getValue(default_val, function (value) {
  754. if (value.match(/[?*;{}]/)) {
  755. throw 'Invalid path';
  756. }
  757. return value;
  758. }, cli.getOptError('a ' + identifier, identifier.toUpperCase()));
  759. }
  760. cli.getArrayValue = function (arr, default_val) {
  761. return cli.getValue(default_val, function (value) {
  762. if (arr.indexOf(value) === -1) {
  763. throw 'Unexpected value';
  764. }
  765. return value;
  766. }, cli.getOptError('either [' + arr.join('|') + ']', 'VALUE'));
  767. }
  768. /**
  769. * Gets all data from STDIN (with optional encoding) and sends it to callback.
  770. *
  771. * @param {String} encoding (optional - default is 'utf8')
  772. * @param {Function} callback
  773. * @api public
  774. */
  775. cli.withStdin = function (encoding, callback) {
  776. if (typeof encoding === 'function') {
  777. callback = encoding;
  778. encoding = 'utf8';
  779. }
  780. var stream = process.openStdin(), data = '';
  781. stream.setEncoding(encoding);
  782. stream.on('data', function (chunk) {
  783. data += chunk;
  784. });
  785. stream.on('end', function () {
  786. callback.apply(cli, [data]);
  787. });
  788. };
  789. /**
  790. * Gets all data from STDIN, splits the data into lines and sends it
  791. * to callback (callback isn't called until all of STDIN is read. To
  792. * process each line as it's received, see the method below
  793. *
  794. * @param {Function} callback
  795. * @api public
  796. */
  797. cli.withStdinLines = function (callback) {
  798. cli.withStdin(function (data) {
  799. var sep = data.indexOf('\r\n') !== -1 ? '\r\n' : '\n';
  800. callback.apply(cli, [data.split(sep), sep]);
  801. });
  802. };
  803. /**
  804. * Asynchronously reads a file line by line. When a line is received,
  805. * callback is called with (line, sep) - when EOF is reached, callback
  806. * receives (null, null, true)
  807. *
  808. * @param {String} file (optional - default is 'stdin')
  809. * @param {String} encoding (optional - default is 'utf8')
  810. * @param {Function} callback (line, sep, eof)
  811. * @api public
  812. */
  813. cli.withInput = function (file, encoding, callback) {
  814. if (typeof encoding === 'function') {
  815. callback = encoding;
  816. encoding = 'utf8';
  817. } else if (typeof file === 'function') {
  818. callback = file;
  819. encoding = 'utf8';
  820. file = 'stdin';
  821. }
  822. if (file === 'stdin') {
  823. file = process.openStdin();
  824. } else {
  825. try {
  826. file = cli.native.fs.createReadStream(file);
  827. file.on('error', cli.fatal);
  828. } catch (e) {
  829. return cli.fatal(e);
  830. }
  831. }
  832. file.setEncoding(encoding);
  833. var lines = [], data = '', eof, sep;
  834. file.on('data', function (chunk) {
  835. if (eof) return;
  836. data += chunk;
  837. if (!sep) {
  838. if (data.indexOf('\r\n') !== -1) {
  839. sep = '\r\n';
  840. } else if (data.indexOf('\n') !== -1) {
  841. sep = '\n';
  842. } else {
  843. last_line = data;
  844. return;
  845. }
  846. }
  847. lines = data.split(sep);
  848. data = eof ? null : lines.pop();
  849. while (lines.length) {
  850. callback.apply(cli, [lines.shift(), sep, false]);
  851. }
  852. });
  853. file.on('end', function () {
  854. eof = true;
  855. if (data.length) {
  856. callback.apply(cli, [data, sep || '', false]);
  857. }
  858. callback.apply(cli, [null, null, true]);
  859. });
  860. };
  861. /**
  862. * This function does a much better job at determining the object type than the typeof operator
  863. * @author Angus Croll - https://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/
  864. * @param {Object} obj A Javascript object you wish to know the type of.
  865. * @return {string} A string describing the Object's type if it is indeed a built in JavaScript type.
  866. */
  867. cli.toType = function(obj) {
  868. var type = ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
  869. function isInt(n) {
  870. return Number(n) === n && n % 1 === 0;
  871. }
  872. function isFloat(n){
  873. return n === Number(n) && n % 1 !== 0;
  874. }
  875. if ( type === 'number' ) {
  876. if ( isInt(obj) ) {
  877. return 'integer';
  878. } else if ( isFloat(obj) ) {
  879. return 'float';
  880. }
  881. }
  882. return type;
  883. }
  884. /**
  885. * The main entry method. `callback` receives (args, options)
  886. *
  887. * @param {Function} callback
  888. * @api public
  889. */
  890. cli.main = function (callback) {
  891. callback.call(cli, cli.args, cli.options);
  892. }
  893. /**
  894. * Bind creationix's stack (https://github.com/creationix/stack).
  895. *
  896. * Create a simple middleware stack by calling:
  897. *
  898. * cli.createServer(middleware).listen(port);
  899. *
  900. * @return {Server} server
  901. * @api public
  902. */
  903. cli.createServer = function(/*layers*/) {
  904. var defaultStackErrorHandler = function (req, res, err) {
  905. if (err) {
  906. console.error(err.stack);
  907. res.writeHead(500, {"Content-Type": "text/plain"});
  908. return res.end(err.stack + "\n");
  909. }
  910. res.writeHead(404, {"Content-Type": "text/plain"});
  911. res.end("Not Found\n");
  912. };
  913. var handle, error;
  914. handle = error = defaultStackErrorHandler;
  915. var layers = Array.prototype.slice.call(arguments);
  916. //Allow createServer(a,b,c) and createServer([a,b,c])
  917. if (layers.length && layers[0] instanceof Array) {
  918. layers = layers[0];
  919. }
  920. layers.reverse().forEach(function (layer) {
  921. var child = handle;
  922. handle = function (req, res) {
  923. try {
  924. layer(req, res, function (err) {
  925. if (err) return error(req, res, err);
  926. child(req, res);
  927. });
  928. } catch (err) {
  929. error(req, res, err);
  930. }
  931. };
  932. });
  933. return cli.native.http.createServer(handle);
  934. };
  935. /**
  936. * A wrapper for child_process.exec().
  937. *
  938. * If the child_process exits successfully, `callback` receives an array of
  939. * stdout lines. The current process exits if the child process has an error
  940. * and `errback` isn't defined.
  941. *
  942. * @param {String} cmd
  943. * @param {Function} callback (optional)
  944. * @param {Function} errback (optional)
  945. * @api public
  946. */
  947. cli.exec = function (cmd, callback, errback) {
  948. cli.native.child_process.exec(cmd, function (err, stdout, stderr) {
  949. err = err || stderr;
  950. if (err) {
  951. if (errback) {
  952. return errback(err, stdout);
  953. }
  954. return cli.fatal('exec() failed\n' + err);
  955. }
  956. if (callback) {
  957. callback(stdout.split('\n'));
  958. }
  959. });
  960. };
  961. /**
  962. * Helper method for outputting a progress bar to the console.
  963. *
  964. * @param {Number} progress (0 <= progress <= 1)
  965. * @api public
  966. */
  967. var last_progress_call, progress_len = 74, min_progress_increase = 5, last_progress_percentage = 0;
  968. cli.progress = function (progress, decimals, stream) {
  969. stream = stream || process.stdout;
  970. if (progress < 0 || progress > 1 || isNaN(progress)) return;
  971. if (!decimals) decimals = 0;
  972. var now = (new Date()).getTime();
  973. if (last_progress_call && (now - last_progress_call) < 100 && progress !== 1) {
  974. return; //Throttle progress calls
  975. }
  976. last_progress_call = now;
  977. var pwr = Math.pow(10, decimals);
  978. var percentage_as_num = Math.floor(progress * 100 * pwr) / pwr;
  979. if (!stream.isTTY && percentage_as_num < 100 && percentage_as_num - last_progress_percentage < min_progress_increase) {
  980. return; //don't over-print if not TTY
  981. }
  982. last_progress_percentage = percentage_as_num;
  983. var percentage = percentage_as_num + '%';
  984. for (var i = 0; i < decimals; i++) {
  985. percentage += ' ';
  986. }
  987. if (!stream.isTTY) {
  988. if (percentage_as_num < 100) {
  989. stream.write(percentage + '...');
  990. }
  991. else {
  992. stream.write(percentage + '\n');
  993. last_progress_percentage = 0;
  994. }
  995. return;
  996. }
  997. var bar_length = Math.floor(progress_len * progress),
  998. str = '';
  999. if (bar_length == 0 && progress > 0) {
  1000. bar_length = 1;
  1001. }
  1002. for (i = 1; i <= progress_len; i++) {
  1003. str += i <= bar_length ? '#' : ' ';
  1004. }
  1005. stream.clearLine();
  1006. stream.write('[' + str + '] ' + percentage);
  1007. if (progress === 1) {
  1008. stream.write('\n');
  1009. } else {
  1010. stream.cursorTo(0);
  1011. }
  1012. };
  1013. /**
  1014. * Helper method for outputting a spinner to the console.
  1015. *
  1016. * @param {String|Boolean} prefix (optional)
  1017. * @api public
  1018. */
  1019. var spinner_interval;
  1020. cli.spinner = function (prefix, end, stream) {
  1021. stream = stream || process.stdout;
  1022. if(!stream.isTTY) {
  1023. stream.write(prefix + '\n');
  1024. return;
  1025. }
  1026. if (end) {
  1027. stream.clearLine();
  1028. stream.cursorTo(0);
  1029. stream.write(prefix + '\n');
  1030. return clearInterval(spinner_interval);
  1031. }
  1032. prefix = prefix + ' ' || '';
  1033. var spinner = ['-','\\','|','/'], i = 0, l = spinner.length;
  1034. spinner_interval = setInterval(function () {
  1035. stream.clearLine();
  1036. stream.cursorTo(0);
  1037. stream.write(prefix + spinner[i++]);
  1038. if (i == l) i = 0;
  1039. }, 200);
  1040. };