@animetosho/parpar
Version:
High performance multi-threaded PAR2 creation library
426 lines (382 loc) • 11.7 kB
JavaScript
"use strict";
var RE_DIGITS = /^\d+$/;
var parseSize = function(s) {
if(typeof s == 'number' || (''+s).search(RE_DIGITS) >= 0) return Math.max(0, Math.floor(s));
var parts = (''+s).toUpperCase().match(/^([0-9.]+)([BKMGTPE])$/);
if(parts) {
var num = +(parts[1]);
switch(parts[2]) {
case 'E': num *= 1024;
case 'P': num *= 1024;
case 'T': num *= 1024;
case 'G': num *= 1024;
case 'M': num *= 1024;
case 'K': num *= 1024;
case 'B': num *= 1;
}
if(isNaN(num)) return false;
return Math.floor(num);
}
return false;
};
var parseTime = function(s) {
if(typeof s == 'number' || (''+s).search(RE_DIGITS) >= 0) return Math.max(0, Math.floor(s*1000));
var parts = (''+s).toLowerCase().match(/^([0-9.]+)(m?s|[mhdw])$/);
if(parts) {
var num = +(parts[1]);
switch(parts[2]) {
case 'w': num *= 7;
case 'd': num *= 24;
case 'h': num *= 60;
case 'm': num *= 60;
case 's': num *= 1000;
}
if(isNaN(num)) return false;
return Math.floor(num);
}
return false;
};
module.exports = function(argv, opts) {
if(!Array.isArray(argv) && typeof argv == 'object')
return parseObject(argv, opts);
var aliasMap = {};
var ret = {_: []};
for(var k in opts) {
if(opts[k].alias)
aliasMap[opts[k].alias] = k;
}
var applyFn = {};
var setKey = function(key, val, explicit) {
var o = opts[key];
if(o === undefined)
throw new Error('Unknown option `' + key + '`');
var isMultiple = (['list','array','map','map2'].indexOf(o.type) !== -1);
if((key in ret) && !isMultiple)
throw new Error('Option `' + key + '` specified more than once');
// special handling for booleans
if(o.type === 'bool') {
if(val === true || val === false) {
ret[key] = val;
} else switch(val.toLowerCase()) {
case 'true':
ret[key] = true;
break;
case 'false':
case '0':
ret[key] = false;
break;
default:
throw new Error('Unexpected value for `' + key + '`');
}
if(o.fn) ret[key] = o.fn(ret[key]);
return;
}
if(!explicit && val !== undefined && val[0] === '-' && val.length > 1)
throw new Error('Potentially incorrect usage - trying to set `' + key + '` to `' + val + '`; if you intend this, please specify `--' + key + '=' + val + '` instead');
if(isMultiple) {
// o.ifSetDefault can only really be handled properly at the end, so defer that
if((val === undefined || (val === '' && !explicit))) {
if(o.ifSetDefault === undefined)
throw new Error('No value specified for `' + key + '`');
else if(key in ret)
throw new Error('No value specified for `' + key + '`');
ret[key] = null; // mark that this wasn't set
return;
} else if(val === false) {
// explicit blank
if(key in ret) {
if(ret[key] === false)
throw new Error('Option `' + key + '` specified more than once');
else
throw new Error('Conflicting values passed for `' + key + '`');
}
ret[key] = false; // fix this later
return;
}
if(!(key in ret))
ret[key] = (o.type == 'map' || o.type == 'map2') ? {} : [];
else if(!ret[key]) { // option set to a special scalar value
if(ret[key] === null)
throw new Error('No value specified for `' + key + '`');
else
throw new Error('Conflicting values passed for `' + key + '`');
}
switch(o.type) {
case 'list':
ret[key] = ret[key].concat(val.split(',').map(function(s) {
return s.trim().toLowerCase();
}));
break;
case 'array':
ret[key].push(val);
break;
case 'map':
case 'map2':
var m;
if(m = val.match(/^(.+?)[=:](.*)$/))
ret[key][m[1].trim()] = m[2].trim();
else if(o.type == 'map2')
ret[key][val.trim()] = undefined;
else
throw new Error('Invalid format for `' + key + '`');
break;
}
if(o.fn) applyFn[key] = 1;
} else {
if(val === undefined || (val === '' && !explicit)) {
if(o.ifSetDefault !== undefined)
val = o.ifSetDefault;
else
throw new Error('No value specified for `' + key + '`');
}
switch(o.type) {
case 'int':
ret[key] = val|0;
if(ret[key] < 0 || !val.match(/^\d+$/)) throw new Error('Invalid number specified for `' + key + '`');
break;
case '-int':
if(!val.match(/^-?\d+$/)) throw new Error('Invalid number specified for `' + key + '`');
ret[key] = val|0;
break;
case 'size':
ret[key] = parseSize(val);
if(!ret[key]) throw new Error('Invalid size specified for `' + key + '`');
break;
case 'size0':
ret[key] = parseSize(val);
if(ret[key] === false) throw new Error('Invalid size specified for `' + key + '`');
break;
case 'time':
ret[key] = parseTime(val);
if(ret[key] === false) throw new Error('Invalid time specified for `' + key + '`');
break;
case 'enum':
if(o.enum.indexOf((''+val).toLowerCase()) === -1)
throw new Error('Invalid value specified for `' + key + '`');
default: // string
ret[key] = val;
}
if(o.fn) ret[key] = o.fn(ret[key]);
}
};
for(var i=0; i<argv.length; i++) {
var arg = argv[i];
if(arg[0] === '-') {
if(arg[1] === '-') {
// long opt
if(arg.length === 2) {
// '--' option -> all remaining args aren't to be parsed
ret._ = ret._.concat(argv.slice(++i));
break;
}
var eq = arg.indexOf('=');
if(arg.substring(2, 5).toLowerCase() === 'no-') { // TODO: consider allowing options which start with 'no-' ?
if(eq !== -1)
throw new Error('Unexpected value specified in `' + arg + '`');
var k = arg.substring(5).toLowerCase();
var opt = opts[k];
if(opt && ['list','array','map','map2','bool'].indexOf(opt.type) === -1)
// note that, for multi-value types, --no-opt explicitly sets a blank array/map
throw new Error('Cannot specify `' + arg + '`');
setKey(k, false, true);
} else {
var k = arg.substring(2);
if(eq === -1) {
k = k.toLowerCase();
var opt = opts[k];
if(opt && opt.type === 'bool')
setKey(k, true, true);
else {
var next = argv[i+1];
if(next === undefined || (next[0] === '-' && next.length > 1))
setKey(k, undefined, false);
else {
setKey(k, next, false);
i++;
}
}
} else
setKey(k.substring(0, eq-2).toLowerCase(), arg.substring(eq+1), true);
}
} else {
// short opt
for(var j=1; j<arg.length; j++) {
var k = aliasMap[arg[j]];
if(!k)
throw new Error('Unknown option specified in `' + arg + '`');
var opt = opts[k];
if(opt.type === 'bool' && arg[j+1] !== '=') {
setKey(k, true, true);
} else {
// treat everything else as the value
j++;
if(arg[j] === undefined) {
// consume next arg for value
var next = argv[i+1];
if(next === undefined || (next[0] === '-' && next.length > 1))
setKey(k, undefined, false);
else {
setKey(k, next, false);
i++;
}
} else {
var explicit = (arg[j] === '=');
if(!explicit && j>2) // have something like `-bkval` where `-b` is a bool and `-k` expects a value, this is vague and may signify user error, so reject this
throw new Error('Ambiguous option `' + arg + '` supplied, as `' + arg[j] + '` (`' + k + '`) expects a value; please check usage');
setKey(k, arg.substring(j + explicit), explicit);
}
break;
}
}
}
} else {
ret._.push(arg);
}
}
// apply functions to multi-valued items
for(var k in applyFn)
ret[k] = opts[k].fn(ret[k]);
// handle defaults + multi-value
for(var k in opts) {
var o = opts[k];
if(o.default !== undefined && !(k in ret))
ret[k] = o.default;
else if((k in ret) && ['list','array','map','map2'].indexOf(o.type) !== -1 && !ret[k])
if(ret[k] === null)
ret[k] = o.ifSetDefault;
else
ret[k] = (o.type == 'map' || o.type == 'map2') ? {} : [];
if(!(k in ret) && o.required)
throw new Error('Missing value for `' + k + '`');
}
return ret;
};
// parse command-line in object form - useful for config files
var parseObject = function(config, opts) {
var ret = {};
for(var k in config) {
var v = config[k];
k = k.toLowerCase();
var opt = opts[k];
if(!opt) continue;
if(k in ret)
throw new Error('Option `' + k + '` specified more than once');
if(opt.type !== 'bool') {
if(v === true && opt.ifSetDefault !== undefined) {
ret[k] = opt.ifSetDefault;
continue;
}
if(v === null && opt.default !== undefined) {
ret[k] = opt.default;
continue;
}
if(v === false || v === null) // treat as unset
continue;
}
// pre-conversion for strings
if(typeof v === 'string') {
switch(opt.type) {
case 'bool':
switch(v.toLowerCase()) {
case 'true':
case '1':
v = true;
break;
case 'false':
case '0':
case '':
v = false;
break;
}
throw new Error('Invalid value specified for `' + k + '`');
case '-int':
case 'int':
if(!v.match(/^-?\d+$/)) throw new Error('Invalid number specified for `' + k + '`');
v = v|0;
break;
case 'size':
case 'size0':
v = parseSize(v);
break;
case 'time':
v = parseTime(v);
if(v === false) throw new Error('Invalid time specified for `' + k + '`');
break;
case 'array':
case 'list': // will be parsed later
case 'map': case 'map2': // will be parsed later
v = [v];
break;
}
}
switch(opt.type) {
case 'bool':
if(v === true || v === false || v === 1 || v === 0 || v === null) {
ret[k] = !!v;
break;
}
throw new Error('Invalid value specified for `' + k + '`');
case 'size':
if(!v) throw new Error('Invalid size specified for `' + k + '`');
case 'size0':
if(v === false) throw new Error('Invalid size specified for `' + k + '`');
case '-int':
case 'int':
case 'time':
if(typeof v === 'number') {
ret[k] = v|0;
if(opt.type === '-int' || ret[k] >= 0) break;
}
throw new Error('Invalid value specified for `' + k + '`');
case 'list':
if(!Array.isArray(v)) throw new Error('Invalid value specified for `' + k + '`');
ret[k] = [];
v.forEach(function(s) {
ret[k] = ret[k].concat(s.toLowerCase().split(',').map(function(s) {
return s.trim();
}));
});
break;
case 'array':
if(!Array.isArray(v)) throw new Error('Invalid value specified for `' + k + '`');
ret[k] = v;
break;
case 'map':
case 'map2':
if(Array.isArray(v)) { // array of strings -> parse to object
ret[k] = {};
v.forEach(function(s) {
if(typeof s !== 'string')
throw new Error('Invalid format for `' + k + '`');
var m;
if(m = s.match(/^(.+?)[=:](.*)$/))
ret[k][m[1].trim()] = m[2].trim();
else if(opt.type == 'map2')
ret[k][s.trim()] = undefined;
else
throw new Error('Invalid format for `' + k + '`');
});
} else if(typeof v === 'object')
ret[k] = v;
else
throw new Error('Invalid value specified for `' + k + '`');
break;
case 'enum':
if(opt.enum.indexOf((''+v).toLowerCase()) === -1)
throw new Error('Invalid value specified for `' + k + '`');
default: // string
ret[k] = v;
}
if(opt.fn) ret[k] = opt.fn(ret[k]);
}
// handle defaults
for(var k in opts) {
var o = opts[k];
if(o.default && !(k in ret))
ret[k] = o.default;
}
return ret;
};
// export some useful parsing functions
module.exports.parseSize = parseSize;
module.exports.parseTime = parseTime;