Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80759 views
1
// fancy-pants parsing of argv, originally forked
2
// from minimist: https://www.npmjs.com/package/minimist
3
var camelCase = require('camelcase'),
4
fs = require('fs'),
5
path = require('path');
6
7
module.exports = function (args, opts) {
8
if (!opts) opts = {};
9
var flags = { arrays: {}, bools : {}, strings : {}, counts: {}, normalize: {}, configs: {} };
10
11
[].concat(opts['array']).filter(Boolean).forEach(function (key) {
12
flags.arrays[key] = true;
13
});
14
15
[].concat(opts['boolean']).filter(Boolean).forEach(function (key) {
16
flags.bools[key] = true;
17
});
18
19
[].concat(opts.string).filter(Boolean).forEach(function (key) {
20
flags.strings[key] = true;
21
});
22
23
[].concat(opts.count).filter(Boolean).forEach(function (key) {
24
flags.counts[key] = true;
25
});
26
27
[].concat(opts.normalize).filter(Boolean).forEach(function (key) {
28
flags.normalize[key] = true;
29
});
30
31
[].concat(opts.config).filter(Boolean).forEach(function (key) {
32
flags.configs[key] = true;
33
});
34
35
var aliases = {},
36
newAliases = {};
37
38
extendAliases(opts.key);
39
extendAliases(opts.alias);
40
41
var defaults = opts['default'] || {};
42
Object.keys(defaults).forEach(function (key) {
43
if (/-/.test(key) && !opts.alias[key]) {
44
var c = camelCase(key);
45
aliases[key] = aliases[key] || [];
46
// don't allow the same key to be added multiple times.
47
if (aliases[key].indexOf(c) === -1) {
48
aliases[key] = (aliases[key] || []).concat(c);
49
newAliases[c] = true;
50
}
51
}
52
(aliases[key] || []).forEach(function (alias) {
53
defaults[alias] = defaults[key];
54
});
55
});
56
57
var argv = { _ : [] };
58
Object.keys(flags.bools).forEach(function (key) {
59
setArg(key, !(key in defaults) ? false : defaults[key]);
60
});
61
62
var notFlags = [];
63
if (args.indexOf('--') !== -1) {
64
notFlags = args.slice(args.indexOf('--')+1);
65
args = args.slice(0, args.indexOf('--'));
66
}
67
68
for (var i = 0; i < args.length; i++) {
69
var arg = args[i];
70
71
// -- seperated by =
72
if (arg.match(/^--.+=/)) {
73
// Using [\s\S] instead of . because js doesn't support the
74
// 'dotall' regex modifier. See:
75
// http://stackoverflow.com/a/1068308/13216
76
var m = arg.match(/^--([^=]+)=([\s\S]*)$/);
77
setArg(m[1], m[2]);
78
}
79
else if (arg.match(/^--no-.+/)) {
80
var key = arg.match(/^--no-(.+)/)[1];
81
setArg(key, false);
82
}
83
// -- seperated by space.
84
else if (arg.match(/^--.+/)) {
85
var key = arg.match(/^--(.+)/)[1];
86
87
if (checkAllAliases(key, opts.narg)) {
88
i = eatNargs(i, key, args);
89
} else {
90
var next = args[i + 1];
91
92
if (next !== undefined && !next.match(/^-/)
93
&& !checkAllAliases(key, flags.bools)
94
&& !checkAllAliases(key, flags.counts)) {
95
setArg(key, next);
96
i++;
97
}
98
else if (/^(true|false)$/.test(next)) {
99
setArg(key, next);
100
i++;
101
}
102
else {
103
setArg(key, defaultForType(guessType(key, flags)));
104
}
105
}
106
}
107
// dot-notation flag seperated by '='.
108
else if (arg.match(/^-.\..+=/)) {
109
var m = arg.match(/^-([^=]+)=([\s\S]*)$/);
110
setArg(m[1], m[2]);
111
}
112
// dot-notation flag seperated by space.
113
else if (arg.match(/^-.\..+/)) {
114
var key = arg.match(/^-(.\..+)/)[1];
115
var next = args[i + 1];
116
if (next !== undefined && !next.match(/^-/)
117
&& !checkAllAliases(key, flags.bools)
118
&& !checkAllAliases(key, flags.counts)) {
119
setArg(key, next);
120
i++;
121
}
122
else {
123
setArg(key, defaultForType(guessType(key, flags)));
124
}
125
}
126
else if (arg.match(/^-[^-]+/)) {
127
var letters = arg.slice(1,-1).split('');
128
129
var broken = false;
130
for (var j = 0; j < letters.length; j++) {
131
var next = arg.slice(j+2);
132
133
if (letters[j+1] && letters[j+1] === '=') {
134
setArg(letters[j], arg.slice(j+3));
135
broken = true;
136
break;
137
}
138
139
if (next === '-') {
140
setArg(letters[j], next)
141
continue;
142
}
143
if (/[A-Za-z]/.test(letters[j])
144
&& /-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) {
145
setArg(letters[j], next);
146
broken = true;
147
break;
148
}
149
if (letters[j+1] && letters[j+1].match(/\W/)) {
150
setArg(letters[j], arg.slice(j+2));
151
broken = true;
152
break;
153
}
154
else {
155
setArg(letters[j], defaultForType(guessType(letters[j], flags)));
156
}
157
}
158
159
var key = arg.slice(-1)[0];
160
161
if (!broken && key !== '-') {
162
if (checkAllAliases(key, opts.narg)) {
163
i = eatNargs(i, key, args);
164
} else {
165
if (args[i+1] && !/^(-|--)[^-]/.test(args[i+1])
166
&& !checkAllAliases(key, flags.bools)
167
&& !checkAllAliases(key, flags.counts)) {
168
setArg(key, args[i+1]);
169
i++;
170
}
171
else if (args[i+1] && /true|false/.test(args[i+1])) {
172
setArg(key, args[i+1]);
173
i++;
174
}
175
else {
176
setArg(key, defaultForType(guessType(key, flags)));
177
}
178
}
179
}
180
}
181
else {
182
argv._.push(
183
flags.strings['_'] || !isNumber(arg) ? arg : Number(arg)
184
);
185
}
186
}
187
188
setConfig(argv);
189
applyDefaultsAndAliases(argv, aliases, defaults);
190
191
Object.keys(flags.counts).forEach(function (key) {
192
setArg(key, defaults[key]);
193
});
194
195
notFlags.forEach(function(key) {
196
argv._.push(key);
197
});
198
199
// how many arguments should we consume, based
200
// on the nargs option?
201
function eatNargs (i, key, args) {
202
var toEat = checkAllAliases(key, opts.narg);
203
204
if (args.length - (i + 1) < toEat) throw Error('not enough arguments following: ' + key);
205
206
for (var ii = i + 1; ii < (toEat + i + 1); ii++) {
207
setArg(key, args[ii]);
208
}
209
210
return (i + toEat);
211
}
212
213
function setArg (key, val) {
214
// handle parsing boolean arguments --foo=true --bar false.
215
if (checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) {
216
if (typeof val === 'string') val = val === 'true';
217
}
218
219
if (/-/.test(key) && !(aliases[key] && aliases[key].length)) {
220
var c = camelCase(key);
221
aliases[key] = [c];
222
newAliases[c] = true;
223
}
224
225
var value = !checkAllAliases(key, flags.strings) && isNumber(val) ? Number(val) : val;
226
227
if (checkAllAliases(key, flags.counts)) {
228
value = function(orig) { return orig !== undefined ? orig + 1 : 0; };
229
}
230
231
var splitKey = key.split('.');
232
setKey(argv, splitKey, value);
233
234
(aliases[splitKey[0]] || []).forEach(function (x) {
235
x = x.split('.');
236
237
// handle populating dot notation for both
238
// the key and its aliases.
239
if (splitKey.length > 1) {
240
var a = [].concat(splitKey);
241
a.shift(); // nuke the old key.
242
x = x.concat(a);
243
}
244
245
setKey(argv, x, value);
246
});
247
248
var keys = [key].concat(aliases[key] || []);
249
for (var i = 0, l = keys.length; i < l; i++) {
250
if (flags.normalize[keys[i]]) {
251
keys.forEach(function(key) {
252
argv.__defineSetter__(key, function(v) {
253
val = path.normalize(v);
254
});
255
256
argv.__defineGetter__(key, function () {
257
return typeof val === 'string' ?
258
path.normalize(val) : val;
259
});
260
});
261
break;
262
}
263
}
264
}
265
266
// set args from config.json file, this should be
267
// applied last so that defaults can be applied.
268
function setConfig (argv) {
269
var configLookup = {};
270
271
// expand defaults/aliases, in-case any happen to reference
272
// the config.json file.
273
applyDefaultsAndAliases(configLookup, aliases, defaults);
274
275
Object.keys(flags.configs).forEach(function(configKey) {
276
var configPath = argv[configKey] || configLookup[configKey];
277
if (configPath) {
278
try {
279
var config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
280
Object.keys(config).forEach(function (key) {
281
// setting arguments via CLI takes precedence over
282
// values within the config file.
283
if (argv[key] === undefined) {
284
delete argv[key];
285
setArg(key, config[key]);
286
}
287
});
288
} catch (ex) {
289
throw Error('invalid json config file: ' + configPath);
290
}
291
}
292
});
293
}
294
295
function applyDefaultsAndAliases(obj, aliases, defaults) {
296
Object.keys(defaults).forEach(function (key) {
297
if (!hasKey(obj, key.split('.'))) {
298
setKey(obj, key.split('.'), defaults[key]);
299
300
(aliases[key] || []).forEach(function (x) {
301
setKey(obj, x.split('.'), defaults[key]);
302
});
303
}
304
});
305
}
306
307
function hasKey (obj, keys) {
308
var o = obj;
309
keys.slice(0,-1).forEach(function (key) {
310
o = (o[key] || {});
311
});
312
313
var key = keys[keys.length - 1];
314
return key in o;
315
}
316
317
function setKey (obj, keys, value) {
318
var o = obj;
319
keys.slice(0,-1).forEach(function (key) {
320
if (o[key] === undefined) o[key] = {};
321
o = o[key];
322
});
323
324
var key = keys[keys.length - 1];
325
if (typeof value === 'function') {
326
o[key] = value(o[key]);
327
}
328
else if (o[key] === undefined && checkAllAliases(key, flags.arrays)) {
329
o[key] = Array.isArray(value) ? value : [value];
330
}
331
else if (o[key] === undefined || typeof o[key] === 'boolean') {
332
o[key] = value;
333
}
334
else if (Array.isArray(o[key])) {
335
o[key].push(value);
336
}
337
else {
338
o[key] = [ o[key], value ];
339
}
340
}
341
342
// extend the aliases list with inferred aliases.
343
function extendAliases (obj) {
344
Object.keys(obj || {}).forEach(function(key) {
345
aliases[key] = [].concat(opts.alias[key] || []);
346
// For "--option-name", also set argv.optionName
347
aliases[key].concat(key).forEach(function (x) {
348
if (/-/.test(x)) {
349
var c = camelCase(x);
350
aliases[key].push(c);
351
newAliases[c] = true;
352
}
353
});
354
aliases[key].forEach(function (x) {
355
aliases[x] = [key].concat(aliases[key].filter(function (y) {
356
return x !== y;
357
}));
358
});
359
});
360
}
361
362
// check if a flag is set for any of a key's aliases.
363
function checkAllAliases (key, flag) {
364
var isSet = false,
365
toCheck = [].concat(aliases[key] || [], key);
366
367
toCheck.forEach(function(key) {
368
if (flag[key]) isSet = flag[key];
369
});
370
371
return isSet;
372
};
373
374
// return a default value, given the type of a flag.,
375
// e.g., key of type 'string' will default to '', rather than 'true'.
376
function defaultForType (type) {
377
var def = {
378
boolean: true,
379
string: '',
380
array: []
381
};
382
383
return def[type];
384
}
385
386
// given a flag, enforce a default type.
387
function guessType (key, flags) {
388
var type = 'boolean';
389
390
if (flags.strings && flags.strings[key]) type = 'string';
391
else if (flags.arrays && flags.arrays[key]) type = 'array';
392
393
return type;
394
}
395
396
function isNumber (x) {
397
if (typeof x === 'number') return true;
398
if (/^0x[0-9a-f]+$/i.test(x)) return true;
399
return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x);
400
}
401
402
return {
403
argv: argv,
404
aliases: aliases,
405
newAliases: newAliases
406
};
407
};
408
409