stdio
Version:
Standard input/output manager for Node.js
270 lines (269 loc) • 10 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const FAILURE = 1;
const POSITIONAL_ARGS_KEY = 'args';
const ERROR_PREFFIX = 'ERROR#GETOPT: ';
function throwError(message) {
throw new Error(ERROR_PREFFIX + message);
}
function getShorteners(options) {
const initialValue = {};
return Object.entries(options).reduce((accum, [key, value]) => {
if (typeof value === 'object' && value.key)
accum[value.key] = key;
return accum;
}, initialValue);
}
function parseOption(config, arg) {
if (!/^--?[a-zA-Z]/.test(arg))
return null;
if (arg.startsWith('--')) {
return [arg.replace(/^--/, '')];
}
const shorteners = getShorteners(config);
return arg
.replace(/^-/, '')
.split('')
.map(letter => shorteners[letter] || letter);
}
function getStateAndReset(state) {
const partial = { [state.activeOption]: state.optionArgs };
Object.assign(state, {
activeOption: '',
remainingArgs: 0,
optionArgs: [],
isMultiple: false,
});
return partial;
}
function postprocess(input) {
const initialValue = {};
return Object.entries(input).reduce((accum, [key, value]) => {
if (Array.isArray(value) && value.length === 1 && key !== POSITIONAL_ARGS_KEY)
accum[key] = value[0];
else
accum[key] = [...value];
return accum;
}, initialValue);
}
function checkRequiredParams(config, input) {
if (config._meta_ && typeof config._meta_ === 'object') {
const { args = 0, minArgs = 0, maxArgs = 0 } = config._meta_;
let providedArgs = 0;
let error = null;
if (Array.isArray(input[POSITIONAL_ARGS_KEY]) && input[POSITIONAL_ARGS_KEY].length > 0) {
providedArgs = input[POSITIONAL_ARGS_KEY].length;
}
if (args && providedArgs !== args) {
error = `${args} positional arguments are required, but ${providedArgs} were provided`;
}
if (minArgs && providedArgs < minArgs) {
error = `At least ${minArgs} positional arguments are required, but ${providedArgs} were provided`;
}
if (maxArgs && providedArgs > maxArgs) {
error = `Max allowed positional arguments is ${maxArgs}, but ${providedArgs} were provided`;
}
if (error)
throwError(error);
}
Object.entries(config).forEach(([key, value]) => {
if (!value || typeof value !== 'object')
return;
if (!input[key] && !value.mandatory && !value.required)
return;
if ((value.mandatory || value.required) && !input[key]) {
throwError(`Missing option: "--${key}"`);
}
if (value.args && value.args !== '*') {
const expectedArgsCount = parseInt(String(value.args));
const argsCount = input[key] ? input[key].length : 0;
if (expectedArgsCount > 0 && expectedArgsCount !== argsCount) {
throwError(`Option "--${key}" requires ${expectedArgsCount} arguments, but ${argsCount} were provided`);
}
}
});
}
function applyDefaults(config, result) {
Object.entries(config).forEach(([key, value]) => {
if (!value || typeof value !== 'object')
return;
if ('default' in value && !(key in result)) {
const values = Array.isArray(value.default) ? value.default : [value.default];
result[key] = values.map(v => (typeof v === 'boolean' ? v : String(v)));
}
});
}
function getopt(config = {}, command) {
const rawArgs = command.slice(2);
const result = {};
const args = [];
const state = {
activeOption: '',
remainingArgs: 0,
optionArgs: [],
isMultiple: false,
};
rawArgs.forEach(arg => {
const parsedOption = parseOption(config, arg);
if (!parsedOption) {
if (state.activeOption) {
state.optionArgs.push(arg);
state.remainingArgs--;
if (!state.remainingArgs || state.isMultiple) {
const isMultiple = state.isMultiple;
const partial = getStateAndReset(state);
Object.entries(partial).forEach(([key, value]) => {
if (isMultiple && result[key])
partial[key] = result[key].concat(value);
});
Object.assign(result, partial);
}
else {
result[state.activeOption] = state.optionArgs;
}
}
else {
args.push(arg);
}
return;
}
parsedOption.forEach(option => {
if (['h', 'help'].includes(option))
throwError('');
let subconfig = config[option];
if (!subconfig) {
throwError(`Unknown option: "${arg}"`);
return;
}
if (typeof subconfig === 'boolean')
subconfig = {};
const isMultiple = !!subconfig.multiple;
if (result[option] && !isMultiple)
throwError(`Option "--${option}" provided many times`);
let expectedArgsCount = subconfig.args;
if (expectedArgsCount === '*')
expectedArgsCount = Infinity;
if (state.activeOption) {
const partial = getStateAndReset(state);
Object.entries(partial).forEach(([key, value]) => {
if (!result[key])
return;
if (isMultiple)
partial[key] = result[key].concat(value);
});
Object.assign(result, partial);
}
if (!expectedArgsCount && !isMultiple) {
result[option] = [true];
return;
}
Object.assign(state, {
activeOption: option,
remainingArgs: expectedArgsCount || 0,
optionArgs: [],
isMultiple,
});
if (!isMultiple)
result[option] = [true];
});
});
if (args.length)
result[POSITIONAL_ARGS_KEY] = args;
applyDefaults(config, result);
checkRequiredParams(config, result);
return postprocess(result);
}
function getHelpMessage(config, programName) {
const strLines = [
'USAGE: node ' + programName + ' [OPTION1] [OPTION2]... arg1 arg2...',
'The following options are supported:',
];
const lines = [];
Object.entries(config).forEach(([key, value]) => {
if (key === '_meta_')
return;
if (typeof value !== 'object' || !value) {
lines.push([' --' + key, '']);
return;
}
let ops = ' ';
if (value.multiple)
value.args = 1;
const argsCount = value.args || 0;
if (value.args === '*') {
ops += '<ARG1>...<ARGN>';
}
else {
for (let i = 0; i < argsCount; i++) {
ops += '<ARG' + (i + 1) + '> ';
}
}
lines.push([
' ' + (value.key ? '-' + value.key + ', --' : '--') + key + ops,
(value.description || '') +
(value.mandatory || value.required ? ' (required)' : '') +
(value.multiple ? ' (multiple)' : '') +
(value.default ? ' ("' + value.default + '" by default)' : ''),
]);
});
const maxLength = lines.reduce((prev, current) => Math.max(current[0].length, prev), 0);
const plainLines = lines.map(line => {
const key = line[0];
const message = line[1];
const padding = new Array(maxLength - key.length + 1).join(' ');
return (key + padding + '\t' + message).trimRight();
});
return strLines.concat(plainLines).join('\n');
}
function preprocessCommand(command) {
const parsed = [];
command.forEach(item => {
if (/^--?[a-zA-Z]+=/.test(item)) {
const part1 = item.split('=')[0];
const part2 = item.replace(part1 + '=', '');
parsed.push(part1);
parsed.push(part2);
}
else {
parsed.push(item);
}
});
return parsed;
}
function checkConfig(config) {
if (config.help)
throw new Error('"--help" option is reserved and cannot be declared in a getopt() call');
Object.values(config).forEach(value => {
if (!value || typeof value !== 'object') {
console.warn('Boolean description of getopt() options is deprecated and will be ' +
'removed in a future "stdio" release. Please, use an object definitions instead.');
return;
}
if (value.key === 'h')
throw new Error('"-h" option is reserved and cannot be declared in a getopt() call');
if (value.mandatory)
console.warn('"mandatory" option is deprecated and will be removed in a ' +
'future "stdio" release. Please, use "required" instead.');
});
}
exports.default = (config, command = process.argv, options) => {
const { exitOnFailure = true, throwOnFailure = false, printOnFailure = true } = options || {};
try {
checkConfig(config);
return getopt(config, preprocessCommand(command));
}
catch (error) {
if (!error.message.startsWith(ERROR_PREFFIX)) {
throw error;
}
const programName = command[1].split('/').pop() || 'program';
const message = (error.message.replace(ERROR_PREFFIX, '') + '\n' + getHelpMessage(config, programName)).trim();
if (printOnFailure)
console.warn(message);
if (exitOnFailure)
process.exit(FAILURE);
if (throwOnFailure)
throw new Error(message);
return null;
}
};