178 lines
5.6 KiB
JavaScript
178 lines
5.6 KiB
JavaScript
// https://unix.stackexchange.com/questions/364383/confusion-about-changing-meaning-of-arguments-and-options-is-there-an-official
|
|
// https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/basedefs/V1_chap12.html#tag_12_01
|
|
|
|
// 'options' are --foo, -f
|
|
//
|
|
// 'option-arguments' are (non-option) things that follow an option, and that the option expects
|
|
// eg. 'filename.txt' in './example --input filename.txt'
|
|
//
|
|
// 'operands' are (non-option) things that follow an option, but aren't option-arguments
|
|
// eg 'filename.txt' in './example --verbose filename.txt'
|
|
|
|
const defaultErrorMessages = {
|
|
missingRequiredOption: (optionName) => `Missing required option '${optionName}'`,
|
|
missingRequiredArgument: (optionName) => `Missing argument for option ${optionName}`,
|
|
tooManyArguments: (optionName) => `Too many arguments for option ${optionName}`,
|
|
}
|
|
|
|
// TODO figure out how to avoid having to use '<options>.opts to access the output of this'
|
|
|
|
module.exports = class Opts {
|
|
constructor(argv) {
|
|
this.opts = get(argv);
|
|
this.synonyms = [];
|
|
}
|
|
|
|
// TODO equip prototype with ability to print directly through `console.log(opts)`
|
|
|
|
/**
|
|
* Declare multiple option flags to be synonymous.
|
|
* Use this if you have short and long names for the same flag
|
|
* eg: '-d' and '--debug'
|
|
*
|
|
* @param {Array<string>} namesWithDashes
|
|
* @returns {void}
|
|
**/
|
|
synonymize(...namesWithDashes) {
|
|
const names = namesWithDashes.map(n => stripDashes(n));
|
|
names.forEach((n) => {
|
|
const others = names.filter(x => x != n)
|
|
this.synonyms[n] = others;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Verify the presence of a required option
|
|
* @param {string} nameWithDashes
|
|
* @param {string} [errorMessage]
|
|
* @returns {Boolean} **True** indicates that the option is present
|
|
**/
|
|
requireOption(nameWithDashes, errorMessage=null) {
|
|
const name = stripDashes(nameWithDashes);
|
|
if (name in this.opts) return true;
|
|
if (name in this.synonyms) {
|
|
const syns = this.synonyms[name];
|
|
const hits = syns.filter(s => s in this.opts ).length;
|
|
if (hits > 0) { return true; }
|
|
}
|
|
throw new Error(errorMessage || defaultErrorMessages.missingRequiredOption(nameWithDashes));
|
|
}
|
|
|
|
/**
|
|
* Verify the presence of arguments for an option that requires them,
|
|
* if that option is present
|
|
* @param {string} nameWithDashes
|
|
* @param {number} [minRequired]
|
|
* @param {number} [maxRequired]
|
|
* @param {string} [tooFewMessage] Error message if there are too few arguments provided
|
|
* @param {string} [tooManyMessage] Error message if there are too many arguments provided
|
|
* @returns {Boolean} **True** indicates that the arguments are present, or that the option is *not* present
|
|
**/
|
|
requireOptionArgument(nameWithDashes, minRequired=1, maxRequired=1, tooFewMessage=null, tooManyMessage=null) {
|
|
let name = stripDashes(nameWithDashes);
|
|
|
|
const checkArgs = (name) => {
|
|
if (typeof this.opts[name] === 'boolean') {
|
|
throw new Error(tooFewMessage || defaultErrorMessages.missingRequiredArgument(nameWithDashes));
|
|
} else if (this.opts[name].length < minRequired) {
|
|
throw new Error(tooFewMessage || defaultErrorMessages.missingRequiredArgument(nameWithDashes));
|
|
} else if (this.opts[name].length > maxRequired) {
|
|
throw new Error(tooManyMessage || defaultErrorMessages.tooManyArguments(nameWithDashes));
|
|
}
|
|
}
|
|
|
|
if (name in this.opts) {
|
|
checkArgs(name);
|
|
}
|
|
if (name in this.synonyms) {
|
|
const syn = this.synonyms[name].filter(s => s in this.opts )[0];
|
|
checkArgs(syn);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
contains(nameWithDashes) {
|
|
const name = stripDashes(nameWithDashes);
|
|
if (name in this.opts) return true;
|
|
if (name in this.synonyms) {
|
|
const syns = this.synonyms[name];
|
|
const hits = syns.filter(s => s in this.opts).length;
|
|
if (hits > 0) { return true; }
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns [ string: (boolean | string[]) ]
|
|
**/
|
|
function get(argv) {
|
|
argv = process.argv.slice(2);
|
|
|
|
// Label everything in argv as either
|
|
// an 'option' or an 'arg_or_operand'
|
|
let mapped = argv.flatMap((a) => {
|
|
// A single long option
|
|
if (a.startsWith('--')) {
|
|
return { arg: a.substring(2), type: 'option' }
|
|
}
|
|
|
|
// A single short option
|
|
if (a.startsWith('-') && (a.length === 2)) {
|
|
return { arg: a.substring(1), type: 'option' }
|
|
}
|
|
|
|
// Multiple short options
|
|
if (a.startsWith('-')) {
|
|
let splitGroup = a.substring(1).split('');
|
|
return splitGroup.map((sg) => { return {arg: sg, type: 'option'} });
|
|
}
|
|
|
|
// An option-argument or operand
|
|
return { arg: a, type: 'arg_or_operand' };
|
|
});
|
|
|
|
// Group consecutive option-args/operands
|
|
let wrapped = mapped.map((a) => [a]);
|
|
let grouped = [];
|
|
wrapped.forEach((a, i) => {
|
|
if (i === 0 || a[0].type === 'option') {
|
|
grouped.push(a);
|
|
return;
|
|
}
|
|
let prev = grouped.pop();
|
|
if (prev[0].type === 'option') {
|
|
grouped.push(prev);
|
|
grouped.push(a);
|
|
return;
|
|
}
|
|
grouped.push(prev.concat(a));
|
|
return;
|
|
});
|
|
|
|
let out = {};
|
|
grouped.forEach((a, i) => {
|
|
let next = grouped[i+1];
|
|
|
|
if ((a[0].type === 'option')
|
|
&& next
|
|
&& (next[0].type === 'arg_or_operand')) {
|
|
let next_shortened = next.map(n => n.arg);
|
|
out[a[0].arg] = next_shortened;
|
|
grouped.splice(i, 1);
|
|
} else {
|
|
let pair = {};
|
|
pair[a[0].arg] = true;
|
|
a.forEach((b) => {
|
|
out[b.arg] = true;
|
|
});
|
|
}
|
|
});
|
|
return out;
|
|
}
|
|
|
|
function stripDashes(optionName) {
|
|
if (optionName.startsWith('--')) return optionName.substring(2);
|
|
if (optionName.startsWith('-')) return optionName.substring(1);
|
|
return optionName;
|
|
} |