UNPKG

14.4 kBJavaScriptView Raw
1var path = require('path');
2var wordwrap = require('wordwrap');
3
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
12var inst = Argv(process.argv.slice(2));
13Object.keys(inst).forEach(function (key) {
14 Argv[key] = typeof inst[key] == 'function'
15 ? inst[key].bind(inst)
16 : inst[key];
17});
18
19var exports = module.exports = Argv;
20function Argv (args, cwd) {
21 var self = {};
22 if (!cwd) cwd = process.cwd();
23
24 self.$0 = process.argv
25 .slice(0,2)
26 .map(function (x) {
27 var b = rebase(cwd, x);
28 return x.match(/^\//) && b.length < x.length
29 ? b : x
30 })
31 .join(' ')
32 ;
33
34 if (process.env._ != undefined && process.argv[1] == process.env._) {
35 self.$0 = process.env._.replace(
36 path.dirname(process.execPath) + '/', ''
37 );
38 }
39
40 var flags = { bools : {}, strings : {} };
41
42 self.boolean = function (bools) {
43 if (!Array.isArray(bools)) {
44 bools = [].slice.call(arguments);
45 }
46
47 bools.forEach(function (name) {
48 flags.bools[name] = true;
49 });
50
51 return self;
52 };
53
54 self.string = function (strings) {
55 if (!Array.isArray(strings)) {
56 strings = [].slice.call(arguments);
57 }
58
59 strings.forEach(function (name) {
60 flags.strings[name] = true;
61 });
62
63 return self;
64 };
65
66 var aliases = {};
67 self.alias = function (x, y) {
68 if (typeof x === 'object') {
69 Object.keys(x).forEach(function (key) {
70 self.alias(key, x[key]);
71 });
72 }
73 else if (Array.isArray(y)) {
74 y.forEach(function (yy) {
75 self.alias(x, yy);
76 });
77 }
78 else {
79 var zs = (aliases[x] || []).concat(aliases[y] || []).concat(x, y);
80 aliases[x] = zs.filter(function (z) { return z != x });
81 aliases[y] = zs.filter(function (z) { return z != y });
82 }
83
84 return self;
85 };
86
87 var demanded = {};
88 self.demand = function (keys) {
89 if (typeof keys == 'number') {
90 if (!demanded._) demanded._ = 0;
91 demanded._ += keys;
92 }
93 else if (Array.isArray(keys)) {
94 keys.forEach(function (key) {
95 self.demand(key);
96 });
97 }
98 else {
99 demanded[keys] = true;
100 }
101
102 return self;
103 };
104
105 var usage;
106 self.usage = function (msg, opts) {
107 if (!opts && typeof msg === 'object') {
108 opts = msg;
109 msg = null;
110 }
111
112 usage = msg;
113
114 if (opts) self.options(opts);
115
116 return self;
117 };
118
119 function fail (msg) {
120 self.showHelp();
121 if (msg) console.error(msg);
122 process.exit(1);
123 }
124
125 var checks = [];
126 self.check = function (f) {
127 checks.push(f);
128 return self;
129 };
130
131 var defaults = {};
132 self.default = function (key, value) {
133 if (typeof key === 'object') {
134 Object.keys(key).forEach(function (k) {
135 self.default(k, key[k]);
136 });
137 }
138 else {
139 defaults[key] = value;
140 }
141
142 return self;
143 };
144
145 var descriptions = {};
146 self.describe = function (key, desc) {
147 if (typeof key === 'object') {
148 Object.keys(key).forEach(function (k) {
149 self.describe(k, key[k]);
150 });
151 }
152 else {
153 descriptions[key] = desc;
154 }
155 return self;
156 };
157
158 self.parse = function (args_) {
159 args = args_;
160 return self.argv;
161 };
162
163 self.option = self.options = function (key, opt) {
164 if (typeof key === 'object') {
165 Object.keys(key).forEach(function (k) {
166 self.options(k, key[k]);
167 });
168 }
169 else {
170 if (opt.alias) self.alias(key, opt.alias);
171 if (opt.demand) self.demand(key);
172 if (typeof opt.default !== 'undefined') {
173 self.default(key, opt.default);
174 }
175
176 if (opt.boolean || opt.type === 'boolean') {
177 self.boolean(key);
178 }
179 if (opt.string || opt.type === 'string') {
180 self.string(key);
181 }
182
183 var desc = opt.describe || opt.description || opt.desc;
184 if (desc) {
185 self.describe(key, desc);
186 }
187 }
188
189 return self;
190 };
191
192 var wrap = null;
193 self.wrap = function (cols) {
194 wrap = cols;
195 return self;
196 };
197
198 self.showHelp = function (fn) {
199 if (!fn) fn = console.error;
200 fn(self.help());
201 };
202
203 self.help = function () {
204 var keys = Object.keys(
205 Object.keys(descriptions)
206 .concat(Object.keys(demanded))
207 .concat(Object.keys(defaults))
208 .reduce(function (acc, key) {
209 if (key !== '_') acc[key] = true;
210 return acc;
211 }, {})
212 );
213
214 var help = keys.length ? [ 'Options:' ] : [];
215
216 if (usage) {
217 help.unshift(usage.replace(/\$0/g, self.$0), '');
218 }
219
220 var switches = keys.reduce(function (acc, key) {
221 acc[key] = [ key ].concat(aliases[key] || [])
222 .map(function (sw) {
223 return (sw.length > 1 ? '--' : '-') + sw
224 })
225 .join(', ')
226 ;
227 return acc;
228 }, {});
229
230 var switchlen = longest(Object.keys(switches).map(function (s) {
231 return switches[s] || '';
232 }));
233
234 var desclen = longest(Object.keys(descriptions).map(function (d) {
235 return descriptions[d] || '';
236 }));
237
238 keys.forEach(function (key) {
239 var kswitch = switches[key];
240 var desc = descriptions[key] || '';
241
242 if (wrap) {
243 desc = wordwrap(switchlen + 4, wrap)(desc)
244 .slice(switchlen + 4)
245 ;
246 }
247
248 var spadding = new Array(
249 Math.max(switchlen - kswitch.length + 3, 0)
250 ).join(' ');
251
252 var dpadding = new Array(
253 Math.max(desclen - desc.length + 1, 0)
254 ).join(' ');
255
256 var type = null;
257
258 if (flags.bools[key]) type = '[boolean]';
259 if (flags.strings[key]) type = '[string]';
260
261 if (!wrap && dpadding.length > 0) {
262 desc += dpadding;
263 }
264
265 var prelude = ' ' + kswitch + spadding;
266 var extra = [
267 type,
268 demanded[key]
269 ? '[required]'
270 : null
271 ,
272 defaults[key] !== undefined
273 ? '[default: ' + JSON.stringify(defaults[key]) + ']'
274 : null
275 ,
276 ].filter(Boolean).join(' ');
277
278 var body = [ desc, extra ].filter(Boolean).join(' ');
279
280 if (wrap) {
281 var dlines = desc.split('\n');
282 var dlen = dlines.slice(-1)[0].length
283 + (dlines.length === 1 ? prelude.length : 0)
284
285 body = desc + (dlen + extra.length > wrap - 2
286 ? '\n'
287 + new Array(wrap - extra.length + 1).join(' ')
288 + extra
289 : new Array(wrap - extra.length - dlen + 1).join(' ')
290 + extra
291 );
292 }
293
294 help.push(prelude + body);
295 });
296
297 help.push('');
298 return help.join('\n');
299 };
300
301 Object.defineProperty(self, 'argv', {
302 get : parseArgs,
303 enumerable : true,
304 });
305
306 function parseArgs () {
307 var argv = { _ : [], $0 : self.$0 };
308 Object.keys(flags.bools).forEach(function (key) {
309 setArg(key, defaults[key] || false);
310 });
311
312 function setArg (key, val) {
313 var num = Number(val);
314 var value = typeof val !== 'string' || isNaN(num) ? val : num;
315 if (flags.strings[key]) value = val;
316
317 setKey(argv, key.split('.'), value);
318
319 (aliases[key] || []).forEach(function (x) {
320 argv[x] = argv[key];
321 });
322 }
323
324 for (var i = 0; i < args.length; i++) {
325 var arg = args[i];
326
327 if (arg === '--') {
328 argv._.push.apply(argv._, args.slice(i + 1));
329 break;
330 }
331 else if (arg.match(/^--.+=/)) {
332 // Using [\s\S] instead of . because js doesn't support the
333 // 'dotall' regex modifier. See:
334 // http://stackoverflow.com/a/1068308/13216
335 var m = arg.match(/^--([^=]+)=([\s\S]*)$/);
336 setArg(m[1], m[2]);
337 }
338 else if (arg.match(/^--no-.+/)) {
339 var key = arg.match(/^--no-(.+)/)[1];
340 setArg(key, false);
341 }
342 else if (arg.match(/^--.+/)) {
343 var key = arg.match(/^--(.+)/)[1];
344 var next = args[i + 1];
345 if (next !== undefined && !next.match(/^-/)
346 && !flags.bools[key]
347 && (aliases[key] ? !flags.bools[aliases[key]] : true)) {
348 setArg(key, next);
349 i++;
350 }
351 else if (/^(true|false)$/.test(next)) {
352 setArg(key, next === 'true');
353 i++;
354 }
355 else {
356 setArg(key, true);
357 }
358 }
359 else if (arg.match(/^-[^-]+/)) {
360 var letters = arg.slice(1,-1).split('');
361
362 var broken = false;
363 for (var j = 0; j < letters.length; j++) {
364 var next = arg.slice(j+2);
365
366 if (next === '-') {
367 setArg(letters[j], next)
368 continue;
369 }
370
371 if (/[A-Za-z]/.test(letters[j])
372 && /-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) {
373 setArg(letters[j], next);
374 break;
375 }
376
377 if (letters[j+1] && letters[j+1].match(/\W/)) {
378 setArg(letters[j], arg.slice(j+2));
379 broken = true;
380 break;
381 }
382 else {
383 setArg(letters[j], true);
384 }
385 }
386
387 var key = arg.slice(-1)[0];
388 if (!broken && key !== '-') {
389
390 if (args[i+1] && !/^(-|--)[^-]/.test(args[i+1])
391 && !flags.bools[key]
392 && (aliases[key] ? !flags.bools[aliases[key]] : true)) {
393 setArg(key, args[i+1]);
394 i++;
395 }
396 else if (args[i+1] && /true|false/.test(args[i+1])) {
397 setArg(key, args[i+1] === 'true');
398 i++;
399 }
400 else {
401 setArg(key, true);
402 }
403 }
404 }
405 else {
406 var n = Number(arg);
407 argv._.push(flags.strings['_'] || isNaN(n) ? arg : n);
408 }
409 }
410
411 Object.keys(defaults).forEach(function (key) {
412 if (!(key in argv)) {
413 argv[key] = defaults[key];
414 if (key in aliases) {
415 argv[aliases[key]] = defaults[key];
416 }
417 }
418 });
419
420 if (demanded._ && argv._.length < demanded._) {
421 fail('Not enough non-option arguments: got '
422 + argv._.length + ', need at least ' + demanded._
423 );
424 }
425
426 var missing = [];
427 Object.keys(demanded).forEach(function (key) {
428 if (!argv[key]) missing.push(key);
429 });
430
431 if (missing.length) {
432 fail('Missing required arguments: ' + missing.join(', '));
433 }
434
435 checks.forEach(function (f) {
436 try {
437 if (f(argv) === false) {
438 fail('Argument check failed: ' + f.toString());
439 }
440 }
441 catch (err) {
442 fail(err)
443 }
444 });
445
446 return argv;
447 }
448
449 function longest (xs) {
450 return Math.max.apply(
451 null,
452 xs.map(function (x) { return x.length })
453 );
454 }
455
456 return self;
457};
458
459// rebase an absolute path to a relative one with respect to a base directory
460// exported for tests
461exports.rebase = rebase;
462function rebase (base, dir) {
463 var ds = path.normalize(dir).split('/').slice(1);
464 var bs = path.normalize(base).split('/').slice(1);
465
466 for (var i = 0; ds[i] && ds[i] == bs[i]; i++);
467 ds.splice(0, i); bs.splice(0, i);
468
469 var p = path.normalize(
470 bs.map(function () { return '..' }).concat(ds).join('/')
471 ).replace(/\/$/,'').replace(/^$/, '.');
472 return p.match(/^[.\/]/) ? p : './' + p;
473};
474
475function setKey (obj, keys, value) {
476 var o = obj;
477 keys.slice(0,-1).forEach(function (key) {
478 if (o[key] === undefined) o[key] = {};
479 o = o[key];
480 });
481
482 var key = keys[keys.length - 1];
483 if (o[key] === undefined || typeof o[key] === 'boolean') {
484 o[key] = value;
485 }
486 else if (Array.isArray(o[key])) {
487 o[key].push(value);
488 }
489 else {
490 o[key] = [ o[key], value ];
491 }
492}