You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

343 lines
9.3 KiB

7 years ago
  1. var path = require('path');
  2. var minimist = require('minimist');
  3. var wordwrap = require('wordwrap');
  4. /* Hack an instance of Argv with process.argv into Argv
  5. so people can do
  6. require('optimist')(['--beeble=1','-z','zizzle']).argv
  7. to parse a list of args and
  8. require('optimist').argv
  9. to get a parsed version of process.argv.
  10. */
  11. var inst = Argv(process.argv.slice(2));
  12. Object.keys(inst).forEach(function (key) {
  13. Argv[key] = typeof inst[key] == 'function'
  14. ? inst[key].bind(inst)
  15. : inst[key];
  16. });
  17. var exports = module.exports = Argv;
  18. function Argv (processArgs, cwd) {
  19. var self = {};
  20. if (!cwd) cwd = process.cwd();
  21. self.$0 = process.argv
  22. .slice(0,2)
  23. .map(function (x) {
  24. var b = rebase(cwd, x);
  25. return x.match(/^\//) && b.length < x.length
  26. ? b : x
  27. })
  28. .join(' ')
  29. ;
  30. if (process.env._ != undefined && process.argv[1] == process.env._) {
  31. self.$0 = process.env._.replace(
  32. path.dirname(process.execPath) + '/', ''
  33. );
  34. }
  35. var options = {
  36. boolean: [],
  37. string: [],
  38. alias: {},
  39. default: []
  40. };
  41. self.boolean = function (bools) {
  42. options.boolean.push.apply(options.boolean, [].concat(bools));
  43. return self;
  44. };
  45. self.string = function (strings) {
  46. options.string.push.apply(options.string, [].concat(strings));
  47. return self;
  48. };
  49. self.default = function (key, value) {
  50. if (typeof key === 'object') {
  51. Object.keys(key).forEach(function (k) {
  52. self.default(k, key[k]);
  53. });
  54. }
  55. else {
  56. options.default[key] = value;
  57. }
  58. return self;
  59. };
  60. self.alias = function (x, y) {
  61. if (typeof x === 'object') {
  62. Object.keys(x).forEach(function (key) {
  63. self.alias(key, x[key]);
  64. });
  65. }
  66. else {
  67. options.alias[x] = (options.alias[x] || []).concat(y);
  68. }
  69. return self;
  70. };
  71. var demanded = {};
  72. self.demand = function (keys) {
  73. if (typeof keys == 'number') {
  74. if (!demanded._) demanded._ = 0;
  75. demanded._ += keys;
  76. }
  77. else if (Array.isArray(keys)) {
  78. keys.forEach(function (key) {
  79. self.demand(key);
  80. });
  81. }
  82. else {
  83. demanded[keys] = true;
  84. }
  85. return self;
  86. };
  87. var usage;
  88. self.usage = function (msg, opts) {
  89. if (!opts && typeof msg === 'object') {
  90. opts = msg;
  91. msg = null;
  92. }
  93. usage = msg;
  94. if (opts) self.options(opts);
  95. return self;
  96. };
  97. function fail (msg) {
  98. self.showHelp();
  99. if (msg) console.error(msg);
  100. process.exit(1);
  101. }
  102. var checks = [];
  103. self.check = function (f) {
  104. checks.push(f);
  105. return self;
  106. };
  107. var descriptions = {};
  108. self.describe = function (key, desc) {
  109. if (typeof key === 'object') {
  110. Object.keys(key).forEach(function (k) {
  111. self.describe(k, key[k]);
  112. });
  113. }
  114. else {
  115. descriptions[key] = desc;
  116. }
  117. return self;
  118. };
  119. self.parse = function (args) {
  120. return parseArgs(args);
  121. };
  122. self.option = self.options = function (key, opt) {
  123. if (typeof key === 'object') {
  124. Object.keys(key).forEach(function (k) {
  125. self.options(k, key[k]);
  126. });
  127. }
  128. else {
  129. if (opt.alias) self.alias(key, opt.alias);
  130. if (opt.demand) self.demand(key);
  131. if (typeof opt.default !== 'undefined') {
  132. self.default(key, opt.default);
  133. }
  134. if (opt.boolean || opt.type === 'boolean') {
  135. self.boolean(key);
  136. }
  137. if (opt.string || opt.type === 'string') {
  138. self.string(key);
  139. }
  140. var desc = opt.describe || opt.description || opt.desc;
  141. if (desc) {
  142. self.describe(key, desc);
  143. }
  144. }
  145. return self;
  146. };
  147. var wrap = null;
  148. self.wrap = function (cols) {
  149. wrap = cols;
  150. return self;
  151. };
  152. self.showHelp = function (fn) {
  153. if (!fn) fn = console.error;
  154. fn(self.help());
  155. };
  156. self.help = function () {
  157. var keys = Object.keys(
  158. Object.keys(descriptions)
  159. .concat(Object.keys(demanded))
  160. .concat(Object.keys(options.default))
  161. .reduce(function (acc, key) {
  162. if (key !== '_') acc[key] = true;
  163. return acc;
  164. }, {})
  165. );
  166. var help = keys.length ? [ 'Options:' ] : [];
  167. if (usage) {
  168. help.unshift(usage.replace(/\$0/g, self.$0), '');
  169. }
  170. var switches = keys.reduce(function (acc, key) {
  171. acc[key] = [ key ].concat(options.alias[key] || [])
  172. .map(function (sw) {
  173. return (sw.length > 1 ? '--' : '-') + sw
  174. })
  175. .join(', ')
  176. ;
  177. return acc;
  178. }, {});
  179. var switchlen = longest(Object.keys(switches).map(function (s) {
  180. return switches[s] || '';
  181. }));
  182. var desclen = longest(Object.keys(descriptions).map(function (d) {
  183. return descriptions[d] || '';
  184. }));
  185. keys.forEach(function (key) {
  186. var kswitch = switches[key];
  187. var desc = descriptions[key] || '';
  188. if (wrap) {
  189. desc = wordwrap(switchlen + 4, wrap)(desc)
  190. .slice(switchlen + 4)
  191. ;
  192. }
  193. var spadding = new Array(
  194. Math.max(switchlen - kswitch.length + 3, 0)
  195. ).join(' ');
  196. var dpadding = new Array(
  197. Math.max(desclen - desc.length + 1, 0)
  198. ).join(' ');
  199. var type = null;
  200. if (options.boolean[key]) type = '[boolean]';
  201. if (options.string[key]) type = '[string]';
  202. if (!wrap && dpadding.length > 0) {
  203. desc += dpadding;
  204. }
  205. var prelude = ' ' + kswitch + spadding;
  206. var extra = [
  207. type,
  208. demanded[key]
  209. ? '[required]'
  210. : null
  211. ,
  212. options.default[key] !== undefined
  213. ? '[default: ' + JSON.stringify(options.default[key]) + ']'
  214. : null
  215. ,
  216. ].filter(Boolean).join(' ');
  217. var body = [ desc, extra ].filter(Boolean).join(' ');
  218. if (wrap) {
  219. var dlines = desc.split('\n');
  220. var dlen = dlines.slice(-1)[0].length
  221. + (dlines.length === 1 ? prelude.length : 0)
  222. body = desc + (dlen + extra.length > wrap - 2
  223. ? '\n'
  224. + new Array(wrap - extra.length + 1).join(' ')
  225. + extra
  226. : new Array(wrap - extra.length - dlen + 1).join(' ')
  227. + extra
  228. );
  229. }
  230. help.push(prelude + body);
  231. });
  232. help.push('');
  233. return help.join('\n');
  234. };
  235. Object.defineProperty(self, 'argv', {
  236. get : function () { return parseArgs(processArgs) },
  237. enumerable : true,
  238. });
  239. function parseArgs (args) {
  240. var argv = minimist(args, options);
  241. argv.$0 = self.$0;
  242. if (demanded._ && argv._.length < demanded._) {
  243. fail('Not enough non-option arguments: got '
  244. + argv._.length + ', need at least ' + demanded._
  245. );
  246. }
  247. var missing = [];
  248. Object.keys(demanded).forEach(function (key) {
  249. if (!argv[key]) missing.push(key);
  250. });
  251. if (missing.length) {
  252. fail('Missing required arguments: ' + missing.join(', '));
  253. }
  254. checks.forEach(function (f) {
  255. try {
  256. if (f(argv) === false) {
  257. fail('Argument check failed: ' + f.toString());
  258. }
  259. }
  260. catch (err) {
  261. fail(err)
  262. }
  263. });
  264. return argv;
  265. }
  266. function longest (xs) {
  267. return Math.max.apply(
  268. null,
  269. xs.map(function (x) { return x.length })
  270. );
  271. }
  272. return self;
  273. };
  274. // rebase an absolute path to a relative one with respect to a base directory
  275. // exported for tests
  276. exports.rebase = rebase;
  277. function rebase (base, dir) {
  278. var ds = path.normalize(dir).split('/').slice(1);
  279. var bs = path.normalize(base).split('/').slice(1);
  280. for (var i = 0; ds[i] && ds[i] == bs[i]; i++);
  281. ds.splice(0, i); bs.splice(0, i);
  282. var p = path.normalize(
  283. bs.map(function () { return '..' }).concat(ds).join('/')
  284. ).replace(/\/$/,'').replace(/^$/, '.');
  285. return p.match(/^[.\/]/) ? p : './' + p;
  286. };