command.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. exports = module.exports = new Command();
  2. exports.Command = Command;
  3. exports.Option = Option;
  4. /**
  5. * 选项参数
  6. * @param flags 选项标识
  7. * @param description 描述信息
  8. * @constructor 选项参数构造器
  9. */
  10. function Option(flags, description) {
  11. this.flags = flags; // 选项标识
  12. this.required = ~flags.indexOf('<'); // 必须,包含<>
  13. this.optional = ~flags.indexOf('['); // 可选,包含[]
  14. this.bool = !~flags.indexOf('-no-'); // 禁用,包含-no-
  15. flags = flags.split(/[ ,|]+/);
  16. if (flags.length > 1 && !/^[[<]/.test(flags[1]))
  17. this.short = flags.shift(); // 短选项
  18. this.long = flags.shift();// 长选项
  19. this.description = description || ''; // 描述信息
  20. }
  21. /**
  22. * 参数名称,以长选项为名字
  23. * @returns {XML|string}
  24. */
  25. Option.prototype.name = function () {
  26. return this.long
  27. .replace('--', '')
  28. .replace('no-', '');
  29. };
  30. /**
  31. * 判断是短选项还是长选项
  32. * @param arg
  33. * @returns {boolean}
  34. */
  35. Option.prototype.is = function (arg) {
  36. return arg === this.short || arg === this.long;
  37. };
  38. /**
  39. * 命令行
  40. * @param name 命令行名称
  41. * @constructor 命令行构造器
  42. */
  43. function Command(name) {
  44. this.options = []; // 选项集合
  45. this._allowUnknownOption = false; // 是否允许未知参数
  46. this._args = []; // 参数集合
  47. this._name = name || ''; // 名称
  48. }
  49. Command.prototype.name = function (str) {
  50. this._name = str;
  51. return this;
  52. };
  53. /**
  54. * 版本号
  55. * @param str 版本号
  56. * @param flags 选项
  57. * @returns {*}
  58. * @api public
  59. */
  60. Command.prototype.version = function (str, flags) {
  61. if (0 === arguments.length)
  62. return this._version;
  63. this._version = str;
  64. flags = flags || '-V, --version';
  65. this.option(flags, 'output the version number');
  66. return this;
  67. };
  68. /**
  69. * 使用说明
  70. * @param str
  71. * @returns {*}
  72. * @api public
  73. */
  74. Command.prototype.usage = function (str) {
  75. var args = this._args.map(function (arg) {
  76. return humanReadableArgName(arg);
  77. });
  78. var usage = '[options]'
  79. + (this._args.length ? ' ' + args.join(' ') : '');
  80. if (0 === arguments.length)
  81. return this._usage || usage;
  82. this._usage = str;
  83. return this;
  84. };
  85. /**
  86. * 命令行选项赋值
  87. * @param flags 选项参数
  88. * @param description 描述
  89. * @param fn
  90. * @param defaultValue
  91. * @returns {Command}
  92. * @api public
  93. */
  94. Command.prototype.option = function (flags, description, fn, defaultValue) {
  95. var self = this
  96. , option = new Option(flags, description)
  97. , oname = option.name()
  98. , name = camelcase(oname);
  99. // 参数为三个
  100. if (typeof fn !== 'function') {
  101. if (fn instanceof RegExp) {
  102. var regex = fn;
  103. fn = function (val, def) {
  104. var m = regex.exec(val);
  105. return m ? m[0] : def;
  106. }
  107. } else {
  108. defaultValue = fn;
  109. fn = null;
  110. }
  111. }
  112. // 为禁用--no-*,可选[optional],必选<required>设置默认值
  113. if (false === option.bool || option.optional || option.required) {
  114. if (false === option.bool)
  115. defaultValue = true; // 默认为true
  116. if (undefined !== defaultValue)
  117. self[name] = defaultValue;
  118. }
  119. // 注册
  120. this.options.push(option);
  121. return this;
  122. };
  123. /**
  124. * 解析参数
  125. * @param argv 参数
  126. * @returns {Command}
  127. * @api public
  128. */
  129. Command.prototype.parse = function (argv) {
  130. // 存储原始参数
  131. this.rawArgs = argv;
  132. // 猜名字
  133. this._name = this._name || argv[0];
  134. // 解析参数
  135. var parsed = this.parseOptions(this.normalize(argv));
  136. var args = this.args = parsed.args;
  137. var result = this.parseArgs(this.args, parsed.unknown);
  138. return result;
  139. };
  140. /**
  141. * 允许未知选项
  142. * @param arg
  143. * @returns {Command}
  144. * @api public
  145. */
  146. Command.prototype.allowUnknownOption = function (arg) {
  147. this._allowUnknownOption = arguments.length === 0 || arg;
  148. return this;
  149. };
  150. /**
  151. * 规范化参数,主要处理多个短选项如:-xvf和长选项:--options=xxx
  152. * @param args
  153. * @returns {Array}
  154. * @api private
  155. */
  156. Command.prototype.normalize = function (args) {
  157. var ret = []
  158. , arg
  159. , lastOpt
  160. , index;
  161. for (var i = 0, len = args.length; i < len; ++i) {
  162. arg = args[i];
  163. if (i > 0) {
  164. lastOpt = this.optionFor(args[i - 1]);
  165. }
  166. if (arg === '--') {
  167. ret = ret.concat(args.slice(i));
  168. break;
  169. } else if (lastOpt && lastOpt.required) {
  170. ret.push(arg);
  171. } else if (arg.length > 1 && '-' === arg[0] && '-' !== arg[1]) {
  172. arg.slice(1).split('').forEach(function (c) {
  173. ret.push('-' + c);
  174. });
  175. } else if (/^--/.test(arg) && ~(index = arg.indexOf('='))) {
  176. ret.push(arg.slice(0, index), arg.slice(index + 1));
  177. } else {
  178. ret.push(arg);
  179. }
  180. }
  181. return ret;
  182. };
  183. /**
  184. * 解析参数
  185. * @param argv
  186. * @returns {{args: Array, unknown: Array}}
  187. * @api private
  188. */
  189. Command.prototype.parseOptions = function (argv) {
  190. var args = []
  191. , len = argv.length
  192. , literal
  193. , option
  194. , arg;
  195. var unknownOptions = [];
  196. // 解析选项
  197. for (var i = 0; i < len; ++i) {
  198. arg = argv[i];
  199. // 参数后为'-- xxx'时表示为文字而非参数
  200. if ('--' === arg) {
  201. literal = true;
  202. continue;
  203. }
  204. if (literal) {
  205. args.push(arg);
  206. continue;
  207. }
  208. // 找到匹配的选项
  209. option = this.optionFor(arg);
  210. // 定义
  211. if (option) {
  212. if (option.required) { // 选项必须
  213. arg = argv[++i];
  214. if (undefined === arg || null === arg)
  215. return this.optionMissingArgument(option);
  216. this[option.name()] = arg;
  217. } else if (option.optional) { // 选项可选
  218. arg = argv[i + 1];
  219. if (undefined === arg || null === arg || ('-' === arg[0] && '-' !== arg)) {
  220. arg = null;
  221. } else {
  222. ++i;
  223. }
  224. this[option.name()] = arg;
  225. } else {
  226. if ('version' !== option.name()) {
  227. this[option.name()] = true;
  228. } else {
  229. this.outputVersion();
  230. this.exit();
  231. }
  232. }
  233. args.push(arg);
  234. continue;
  235. }
  236. // 未知选项
  237. if (arg.length > 1 && '-' === arg[0]) {
  238. unknownOptions.push(arg);
  239. if (argv[i + 1] && '-' !== argv[i + 1][0]) {
  240. unknownOptions.push(argv[++i]);
  241. }
  242. continue;
  243. }
  244. args.push(arg);
  245. }
  246. return {args: args, unknown: unknownOptions};
  247. };
  248. /**
  249. * 参数匹配选项
  250. * @param arg
  251. * @returns {*}
  252. * @api private
  253. */
  254. Command.prototype.optionFor = function (arg) {
  255. for (var i = 0, len = this.options.length; i < len; ++i) {
  256. if (this.options[i].is(arg)) {
  257. return this.options[i];
  258. }
  259. }
  260. };
  261. /**
  262. * 解析参数
  263. * @param args
  264. * @param unknown
  265. * @returns {Command}
  266. */
  267. Command.prototype.parseArgs = function (args, unknown) {
  268. var name;
  269. if (args.length) {
  270. name = args[0];
  271. } else {
  272. outputHelpIfNecessary(this, unknown);
  273. if (unknown.length > 0) {
  274. this.unknownOption(unknown[0]);
  275. }
  276. }
  277. return this;
  278. };
  279. /**
  280. * 未知选项
  281. * @param flag
  282. */
  283. Command.prototype.unknownOption = function (flag) {
  284. if (this._allowUnknownOption)
  285. return;
  286. console.error();
  287. console.error("error: unknown option `%s'", flag);
  288. console.error();
  289. this.exit();
  290. };
  291. Command.prototype.optionMissingArgument = function (option, flag) {
  292. console.error();
  293. if (flag) {
  294. console.error("error: option `%s' argument missing, got `%s'", option.flags, flag);
  295. } else {
  296. console.error("error: option `%s' argument missing", option.flags);
  297. }
  298. console.error();
  299. this.exit();
  300. };
  301. Command.prototype.outputVersion = function () {
  302. console.log(this._version);
  303. };
  304. Command.prototype.outputHelp = function (cb) {
  305. if (!cb) {
  306. cb = function (passthru) {
  307. return passthru;
  308. }
  309. }
  310. console.log(cb(this.helpInformation()));
  311. };
  312. Command.prototype.helpInformation = function () {
  313. var desc = [];
  314. if (this._description) {
  315. desc = [
  316. ' ' + this._description
  317. , ''
  318. ];
  319. }
  320. var cmdName = this._name;
  321. if (this._alias) {
  322. cmdName = cmdName + '|' + this._alias;
  323. }
  324. var usage = [
  325. ''
  326. , ' Usage: ' + cmdName + ' ' + this.usage()
  327. , ''
  328. ];
  329. var options = [
  330. ' Options:'
  331. , ''
  332. , '' + this.optionHelp().replace(/^/gm, ' ')
  333. , ''
  334. , ''
  335. ];
  336. return usage
  337. .concat(desc)
  338. .concat(options)
  339. .join('\n');
  340. };
  341. Command.prototype.optionHelp = function () {
  342. var width = this.largestOptionLength();
  343. return [pad('-h, --help', width) + ' ' + 'output usage information']
  344. .concat(this.options.map(function (option) {
  345. return pad(option.flags, width) + ' ' + option.description;
  346. }))
  347. .join('\n');
  348. };
  349. Command.prototype.largestOptionLength = function () {
  350. return this.options.reduce(function (max, option) {
  351. return Math.max(max, option.flags.length);
  352. }, 0);
  353. };
  354. Command.prototype.exit = function () {
  355. process.exit();
  356. };
  357. function camelcase(flag) {
  358. return flag.split('-').reduce(function (str, word) {
  359. return str + word[0].toUpperCase() + word.slice(1);
  360. });
  361. }
  362. function outputHelpIfNecessary(cmd, options) {
  363. options = options || [];
  364. for (var i = 0; i < options.length; i++) {
  365. if (options[i] === '--help' || options[i] === '-h') {
  366. cmd.outputHelp();
  367. cmd.exit();
  368. }
  369. }
  370. }
  371. function humanReadableArgName(arg) {
  372. var nameOutput = arg.name + (arg.variadic === true ? '...' : '');
  373. return arg.required
  374. ? '<' + nameOutput + '>'
  375. : '[' + nameOutput + ']'
  376. }
  377. function pad(str, width) {
  378. var len = Math.max(0, width - str.length);
  379. return str + new Array(len + 1).join(' ');
  380. }