nomnom.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. var _ = require("underscore"), chalk = require('chalk');
  2. function ArgParser() {
  3. this.commands = {}; // expected commands
  4. this.specs = {}; // option specifications
  5. }
  6. ArgParser.prototype = {
  7. /* Add a command to the expected commands */
  8. command : function(name) {
  9. var command;
  10. if (name) {
  11. command = this.commands[name] = {
  12. name: name,
  13. specs: {}
  14. };
  15. }
  16. else {
  17. command = this.fallback = {
  18. specs: {}
  19. };
  20. }
  21. // facilitates command('name').options().cb().help()
  22. var chain = {
  23. options : function(specs) {
  24. command.specs = specs;
  25. return chain;
  26. },
  27. opts : function(specs) {
  28. // old API
  29. return this.options(specs);
  30. },
  31. option : function(name, spec) {
  32. command.specs[name] = spec;
  33. return chain;
  34. },
  35. callback : function(cb) {
  36. command.cb = cb;
  37. return chain;
  38. },
  39. help : function(help) {
  40. command.help = help;
  41. return chain;
  42. },
  43. usage : function(usage) {
  44. command._usage = usage;
  45. return chain;
  46. }
  47. };
  48. return chain;
  49. },
  50. nocommand : function() {
  51. return this.command();
  52. },
  53. options : function(specs) {
  54. this.specs = specs;
  55. return this;
  56. },
  57. opts : function(specs) {
  58. // old API
  59. return this.options(specs);
  60. },
  61. globalOpts : function(specs) {
  62. // old API
  63. return this.options(specs);
  64. },
  65. option : function(name, spec) {
  66. this.specs[name] = spec;
  67. return this;
  68. },
  69. usage : function(usage) {
  70. this._usage = usage;
  71. return this;
  72. },
  73. printer : function(print) {
  74. this.print = print;
  75. return this;
  76. },
  77. script : function(script) {
  78. this._script = script;
  79. return this;
  80. },
  81. scriptName : function(script) {
  82. // old API
  83. return this.script(script);
  84. },
  85. help : function(help) {
  86. this._help = help;
  87. return this;
  88. },
  89. colors: function() {
  90. // deprecated - colors are on by default now
  91. return this;
  92. },
  93. nocolors : function() {
  94. this._nocolors = true;
  95. return this;
  96. },
  97. parseArgs : function(argv) {
  98. // old API
  99. return this.parse(argv);
  100. },
  101. nom : function(argv) {
  102. return this.parse(argv);
  103. },
  104. parse : function(argv) {
  105. this.print = this.print || function(str, code) {
  106. console.log(str);
  107. process.exit(code || 0);
  108. };
  109. this._help = this._help || "";
  110. this._script = this._script || process.argv[0] + " "
  111. + require('path').basename(process.argv[1]);
  112. this.specs = this.specs || {};
  113. var argv = argv || process.argv.slice(2);
  114. var arg = Arg(argv[0]).isValue && argv[0],
  115. command = arg && this.commands[arg],
  116. commandExpected = !_(this.commands).isEmpty();
  117. if (commandExpected) {
  118. if (command) {
  119. _(this.specs).extend(command.specs);
  120. this._script += " " + command.name;
  121. if (command.help) {
  122. this._help = command.help;
  123. }
  124. this.command = command;
  125. }
  126. else if (arg) {
  127. return this.print(this._script + ": no such command '" + arg + "'", 1);
  128. }
  129. else {
  130. // no command but command expected e.g. 'git -v'
  131. var helpStringBuilder = {
  132. list : function() {
  133. return 'one of: ' + _(this.commands).keys().join(", ");
  134. },
  135. twoColumn : function() {
  136. // find the longest command name to ensure horizontal alignment
  137. var maxLength = _(this.commands).max(function (cmd) {
  138. return cmd.name.length;
  139. }).name.length;
  140. // create the two column text strings
  141. var cmdHelp = _.map(this.commands, function(cmd, name) {
  142. var diff = maxLength - name.length;
  143. var pad = new Array(diff + 4).join(" ");
  144. return " " + [ name, pad, cmd.help ].join(" ");
  145. });
  146. return "\n" + cmdHelp.join("\n");
  147. }
  148. };
  149. // if there are a small number of commands and all have help strings,
  150. // display them in a two column table; otherwise use the brief version.
  151. // The arbitrary choice of "20" comes from the number commands git
  152. // displays as "common commands"
  153. var helpType = 'list';
  154. if (_(this.commands).size() <= 20) {
  155. if (_(this.commands).every(function (cmd) { return cmd.help; })) {
  156. helpType = 'twoColumn';
  157. }
  158. }
  159. this.specs.command = {
  160. position: 0,
  161. help: helpStringBuilder[helpType].call(this)
  162. }
  163. if (this.fallback) {
  164. _(this.specs).extend(this.fallback.specs);
  165. this._help = this.fallback.help;
  166. } else {
  167. this.specs.command.required = true;
  168. }
  169. }
  170. }
  171. if (this.specs.length === undefined) {
  172. // specs is a hash not an array
  173. this.specs = _(this.specs).map(function(opt, name) {
  174. opt.name = name;
  175. return opt;
  176. });
  177. }
  178. this.specs = this.specs.map(function(opt) {
  179. return Opt(opt);
  180. });
  181. if (argv.indexOf("--help") >= 0 || argv.indexOf("-h") >= 0) {
  182. return this.print(this.getUsage());
  183. }
  184. var options = {};
  185. var args = argv.map(function(arg) {
  186. return Arg(arg);
  187. })
  188. .concat(Arg());
  189. var positionals = [];
  190. /* parse the args */
  191. var that = this;
  192. args.reduce(function(arg, val) {
  193. /* positional */
  194. if (arg.isValue) {
  195. positionals.push(arg.value);
  196. }
  197. else if (arg.chars) {
  198. var last = arg.chars.pop();
  199. /* -cfv */
  200. (arg.chars).forEach(function(ch) {
  201. that.setOption(options, ch, true);
  202. });
  203. /* -v key */
  204. if (!that.opt(last).flag) {
  205. if (val.isValue) {
  206. that.setOption(options, last, val.value);
  207. return Arg(); // skip next turn - swallow arg
  208. }
  209. else {
  210. that.print("'-" + (that.opt(last).name || last) + "'"
  211. + " expects a value\n\n" + that.getUsage(), 1);
  212. }
  213. }
  214. else {
  215. /* -v */
  216. that.setOption(options, last, true);
  217. }
  218. }
  219. else if (arg.full) {
  220. var value = arg.value;
  221. /* --key */
  222. if (value === undefined) {
  223. /* --key value */
  224. if (!that.opt(arg.full).flag) {
  225. if (val.isValue) {
  226. that.setOption(options, arg.full, val.value);
  227. return Arg();
  228. }
  229. else {
  230. that.print("'--" + (that.opt(arg.full).name || arg.full) + "'"
  231. + " expects a value\n\n" + that.getUsage(), 1);
  232. }
  233. }
  234. else {
  235. /* --flag */
  236. value = true;
  237. }
  238. }
  239. that.setOption(options, arg.full, value);
  240. }
  241. return val;
  242. });
  243. positionals.forEach(function(pos, index) {
  244. this.setOption(options, index, pos);
  245. }, this);
  246. options._ = positionals;
  247. this.specs.forEach(function(opt) {
  248. if (opt.default !== undefined && options[opt.name] === undefined) {
  249. options[opt.name] = opt.default;
  250. }
  251. }, this);
  252. // exit if required arg isn't present
  253. this.specs.forEach(function(opt) {
  254. if (opt.required && options[opt.name] === undefined) {
  255. var msg = opt.name + " argument is required";
  256. msg = this._nocolors ? msg : chalk.red(msg);
  257. this.print("\n" + msg + "\n" + this.getUsage(), 1);
  258. }
  259. }, this);
  260. if (command && command.cb) {
  261. command.cb(options);
  262. }
  263. else if (this.fallback && this.fallback.cb) {
  264. this.fallback.cb(options);
  265. }
  266. return options;
  267. },
  268. getUsage : function() {
  269. if (this.command && this.command._usage) {
  270. return this.command._usage;
  271. }
  272. else if (this.fallback && this.fallback._usage) {
  273. return this.fallback._usage;
  274. }
  275. if (this._usage) {
  276. return this._usage;
  277. }
  278. // todo: use a template
  279. var str = "\n"
  280. if (!this._nocolors) {
  281. str += chalk.bold("Usage:");
  282. }
  283. else {
  284. str += "Usage:";
  285. }
  286. str += " " + this._script;
  287. var positionals = _(this.specs).select(function(opt) {
  288. return opt.position != undefined;
  289. })
  290. positionals = _(positionals).sortBy(function(opt) {
  291. return opt.position;
  292. });
  293. var options = _(this.specs).select(function(opt) {
  294. return opt.position === undefined;
  295. });
  296. // assume there are no gaps in the specified pos. args
  297. positionals.forEach(function(pos) {
  298. str += " ";
  299. var posStr = pos.string;
  300. if (!posStr) {
  301. posStr = pos.name || "arg" + pos.position;
  302. if (pos.required) {
  303. posStr = "<" + posStr + ">";
  304. } else {
  305. posStr = "[" + posStr + "]";
  306. }
  307. if (pos.list) {
  308. posStr += "...";
  309. }
  310. }
  311. str += posStr;
  312. });
  313. if (options.length) {
  314. if (!this._nocolors) {
  315. // must be a better way to do this
  316. str += chalk.blue(" [options]");
  317. }
  318. else {
  319. str += " [options]";
  320. }
  321. }
  322. if (options.length || positionals.length) {
  323. str += "\n\n";
  324. }
  325. function spaces(length) {
  326. var spaces = "";
  327. for (var i = 0; i < length; i++) {
  328. spaces += " ";
  329. }
  330. return spaces;
  331. }
  332. var longest = positionals.reduce(function(max, pos) {
  333. return pos.name.length > max ? pos.name.length : max;
  334. }, 0);
  335. positionals.forEach(function(pos) {
  336. var posStr = pos.string || pos.name;
  337. str += posStr + spaces(longest - posStr.length) + " ";
  338. if (!this._nocolors) {
  339. str += chalk.grey(pos.help || "")
  340. }
  341. else {
  342. str += (pos.help || "")
  343. }
  344. str += "\n";
  345. }, this);
  346. if (positionals.length && options.length) {
  347. str += "\n";
  348. }
  349. if (options.length) {
  350. if (!this._nocolors) {
  351. str += chalk.blue("Options:");
  352. }
  353. else {
  354. str += "Options:";
  355. }
  356. str += "\n"
  357. var longest = options.reduce(function(max, opt) {
  358. return opt.string.length > max && !opt.hidden ? opt.string.length : max;
  359. }, 0);
  360. options.forEach(function(opt) {
  361. if (!opt.hidden) {
  362. str += " " + opt.string + spaces(longest - opt.string.length) + " ";
  363. var defaults = (opt.default != null ? " [" + opt.default + "]" : "");
  364. var help = opt.help ? opt.help + defaults : "";
  365. str += this._nocolors ? help: chalk.grey(help);
  366. str += "\n";
  367. }
  368. }, this);
  369. }
  370. if (this._help) {
  371. str += "\n" + this._help;
  372. }
  373. return str;
  374. }
  375. };
  376. ArgParser.prototype.opt = function(arg) {
  377. // get the specified opt for this parsed arg
  378. var match = Opt({});
  379. this.specs.forEach(function(opt) {
  380. if (opt.matches(arg)) {
  381. match = opt;
  382. }
  383. });
  384. return match;
  385. };
  386. ArgParser.prototype.setOption = function(options, arg, value) {
  387. var option = this.opt(arg);
  388. if (option.callback) {
  389. var message = option.callback(value);
  390. if (typeof message == "string") {
  391. this.print(message, 1);
  392. }
  393. }
  394. if (option.type != "string") {
  395. try {
  396. // infer type by JSON parsing the string
  397. value = JSON.parse(value)
  398. }
  399. catch(e) {}
  400. }
  401. if (option.transform) {
  402. value = option.transform(value);
  403. }
  404. var name = option.name || arg;
  405. if (option.choices && option.choices.indexOf(value) == -1) {
  406. this.print(name + " must be one of: " + option.choices.join(", "), 1);
  407. }
  408. if (option.list) {
  409. if (!options[name]) {
  410. options[name] = [value];
  411. }
  412. else {
  413. options[name].push(value);
  414. }
  415. }
  416. else {
  417. options[name] = value;
  418. }
  419. };
  420. /* an arg is an item that's actually parsed from the command line
  421. e.g. "-l", "log.txt", or "--logfile=log.txt" */
  422. var Arg = function(str) {
  423. var abbrRegex = /^\-(\w+?)$/,
  424. fullRegex = /^\-\-(no\-)?(.+?)(?:=(.+))?$/,
  425. valRegex = /^[^\-].*/;
  426. var charMatch = abbrRegex.exec(str),
  427. chars = charMatch && charMatch[1].split("");
  428. var fullMatch = fullRegex.exec(str),
  429. full = fullMatch && fullMatch[2];
  430. var isValue = str !== undefined && (str === "" || valRegex.test(str));
  431. var value;
  432. if (isValue) {
  433. value = str;
  434. }
  435. else if (full) {
  436. value = fullMatch[1] ? false : fullMatch[3];
  437. }
  438. return {
  439. str: str,
  440. chars: chars,
  441. full: full,
  442. value: value,
  443. isValue: isValue
  444. }
  445. }
  446. /* an opt is what's specified by the user in opts hash */
  447. var Opt = function(opt) {
  448. var strings = (opt.string || "").split(","),
  449. abbr, full, metavar;
  450. for (var i = 0; i < strings.length; i++) {
  451. var string = strings[i].trim(),
  452. matches;
  453. if (matches = string.match(/^\-([^-])(?:\s+(.*))?$/)) {
  454. abbr = matches[1];
  455. metavar = matches[2];
  456. }
  457. else if (matches = string.match(/^\-\-(.+?)(?:[=\s]+(.+))?$/)) {
  458. full = matches[1];
  459. metavar = metavar || matches[2];
  460. }
  461. }
  462. matches = matches || [];
  463. var abbr = opt.abbr || abbr, // e.g. v from -v
  464. full = opt.full || full, // e.g. verbose from --verbose
  465. metavar = opt.metavar || metavar; // e.g. PATH from '--config=PATH'
  466. var string;
  467. if (opt.string) {
  468. string = opt.string;
  469. }
  470. else if (opt.position === undefined) {
  471. string = "";
  472. if (abbr) {
  473. string += "-" + abbr;
  474. if (metavar)
  475. string += " " + metavar
  476. string += ", ";
  477. }
  478. string += "--" + (full || opt.name);
  479. if (metavar) {
  480. string += " " + metavar;
  481. }
  482. }
  483. opt = _(opt).extend({
  484. name: opt.name || full || abbr,
  485. string: string,
  486. abbr: abbr,
  487. full: full,
  488. metavar: metavar,
  489. matches: function(arg) {
  490. return opt.full == arg || opt.abbr == arg || opt.position == arg
  491. || opt.name == arg || (opt.list && arg >= opt.position);
  492. }
  493. });
  494. return opt;
  495. }
  496. var createParser = function() {
  497. return new ArgParser();
  498. }
  499. var nomnom = createParser();
  500. for (var i in nomnom) {
  501. if (typeof nomnom[i] == "function") {
  502. createParser[i] = _(nomnom[i]).bind(nomnom);
  503. }
  504. }
  505. module.exports = createParser;