Initial commit
This commit is contained in:
commit
aef5a62455
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
**/node_modules/
|
||||
781
index.js
Normal file
781
index.js
Normal file
@ -0,0 +1,781 @@
|
||||
|
||||
const { StringConsumer } = require('../OxLib/utils/utils')
|
||||
const { ok } = require('node:assert');
|
||||
|
||||
/**
|
||||
* @interface
|
||||
* @template {FlagsI} Flags
|
||||
* @typedef {import('./types.d.ts').ParserOpts<Flags>} ParserOpts
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./types.d.ts').DefaultParserOptsType} DefaultParserOptsType
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template Z
|
||||
* @typedef {import('./types.d.ts').FlagT<T, Z>} FlagT
|
||||
*/
|
||||
|
||||
/**
|
||||
* @interface
|
||||
* @typedef {import('./types.d.ts').FlagsI} FlagsI
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./types.d.ts').JSTypes} JSTypes
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./types.d.ts').FlagAny} FlagAny
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {JSTypes} type
|
||||
* @param {any} value
|
||||
* @param {boolean} [acceptsUndefined=false]
|
||||
* @param {boolean} [acceptsNull=false]
|
||||
*/
|
||||
function expectType(key, type, value, acceptsUndefined, acceptsNull) {
|
||||
if(typeof acceptsNull !== 'boolean')
|
||||
acceptsNull = false;
|
||||
if(typeof acceptsUndefined !== 'boolean')
|
||||
acceptsUndefined = false;
|
||||
if(!acceptsUndefined && typeof value === 'undefined')
|
||||
throw new TypeError(`Required type ${type} for ${key} is undefined!`);
|
||||
if(typeof value === 'undefined') {
|
||||
if(!acceptsUndefined)
|
||||
throw new TypeError(`Type ${type} for ${key} does not accept null values!`);
|
||||
else
|
||||
return;
|
||||
}
|
||||
if(typeof value !== type)
|
||||
throw new TypeError(`Expected ${key} to be ${type}, received ${typeof value}!`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {Object} obj
|
||||
* @param {obj} obj
|
||||
* @param {keyof obj} keyName
|
||||
* @param {JSTypes} keyType
|
||||
* @param {any} defaultVal
|
||||
* @param {string} [objName='opts']
|
||||
*/
|
||||
function validateOrDefaultIfUnset(obj, keyName, keyType, defaultVal, objName) {
|
||||
if(typeof objName === 'undefined')
|
||||
objName = 'otps';
|
||||
if(typeof obj[keyName] === 'undefined')
|
||||
obj[keyName] = defaultVal;
|
||||
expectType(`${objName}.${String(keyName)}`, keyType, obj[keyName], true, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {Object} obj
|
||||
* @param {string} objName
|
||||
* @param {obj} obj
|
||||
* @param {keyof obj} keyName
|
||||
* @param {JSTypes} keyType
|
||||
* @param {any} defaultVal
|
||||
*/
|
||||
function namedValidateOrDefaultIfUnset(objName, obj, keyName, keyType, defaultVal) {
|
||||
validateOrDefaultIfUnset(obj, keyName, keyType, defaultVal, objName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @authorsComments: Not my prettiest work but it works so hey
|
||||
* @throws
|
||||
* @param {DefaultParserOptsType} opts
|
||||
*/
|
||||
function validateAndFillDefaults(opts) {
|
||||
|
||||
const allowedTypeofDefaultType = ["string", "number", "boolean"];
|
||||
const validFlagTypes = ['any', ...allowedTypeofDefaultType];
|
||||
|
||||
const allowedBehaviorOnInputErrorTypes = ['throw', 'log', 'log&exit'];
|
||||
|
||||
validateOrDefaultIfUnset(opts, 'warningLogger', 'function', console.log);
|
||||
validateOrDefaultIfUnset(opts, 'behaviorOnInputError', 'string', 'throw');
|
||||
if(!allowedBehaviorOnInputErrorTypes.includes(opts.behaviorOnInputError))
|
||||
throw new TypeError(`Expected opts.behaviorOnInputError === ${allowedBehaviorOnInputErrorTypes.map((type) => `"${type}"`).join(" | ")}, received "${opts.behaviorOnInputError}"!`);
|
||||
validateOrDefaultIfUnset(opts, 'defaultFlagType', 'string', 'any');
|
||||
if(!validFlagTypes.includes(opts.defaultFlagType))
|
||||
throw new TypeError(`Expected opts.defaultFlagType === ${validFlagTypes.map((type) => `"${type}"`).join(" | ")}, received "${opts.defaultFlagType}"!`);
|
||||
|
||||
/** Category: @default false */
|
||||
validateOrDefaultIfUnset(opts, 'allowSingularDashLongFlags', 'boolean', false);
|
||||
|
||||
/** Category: @default true */
|
||||
validateOrDefaultIfUnset(opts, 'allowUnknownFlags', 'boolean', true);
|
||||
validateOrDefaultIfUnset(opts, 'lowerCaseFlags', 'boolean', true);
|
||||
validateOrDefaultIfUnset(opts, 'resolveFlagValueTypes', 'boolean', true);
|
||||
validateOrDefaultIfUnset(opts, 'allowNullDefaultFlagValues', 'boolean', true);
|
||||
|
||||
|
||||
validateOrDefaultIfUnset(opts, 'automaticBooleanFlagNegation', 'object', {});
|
||||
namedValidateOrDefaultIfUnset('opts.automaticBooleanFlagNegation', opts.automaticBooleanFlagNegation, 'allowUnspacedNegatedFlags', 'boolean', true);
|
||||
namedValidateOrDefaultIfUnset('opts.automaticBooleanFlagNegation', opts.automaticBooleanFlagNegation, 'enabled', 'boolean', true);
|
||||
namedValidateOrDefaultIfUnset('opts.automaticBooleanFlagNegation', opts.automaticBooleanFlagNegation, 'shorthandNegation', 'boolean', true);
|
||||
|
||||
validateOrDefaultIfUnset(opts, 'flags', "object", {});
|
||||
|
||||
|
||||
Object.entries(opts.flags).forEach(/** @param {[string, FlagAny]} value */ ([flagName, flagData]) => {
|
||||
// namedValidateOrDefaultIfUnset('flag', flagData, 'default')
|
||||
expectType(`flag["${flagName}"]`, 'object', flagData, false, false);
|
||||
|
||||
if(typeof flagData.type === 'undefined') {
|
||||
opts.warningLogger(`warn: flag["${flagName}"].type is undefined, defaulting to '${opts.defaultFlagType}'`);
|
||||
flagData.type = opts.defaultFlagType;
|
||||
}
|
||||
|
||||
if(!validFlagTypes.includes(flagData.type)) {
|
||||
throw new TypeError(`Expected flag["${flagName}"].type === ${validFlagTypes.map((type) => `"${type}"`).join(" | ")}, received "${flagData.type}"!`);
|
||||
}
|
||||
|
||||
/** Type of flagData.default should be consistent with flagData.type */
|
||||
if(flagData.default !== undefined) {
|
||||
if(flagData.default !== null && opts.allowNullDefaultFlagValues) {
|
||||
if(flagData.type !== 'any' && typeof flagData.default !== flagData.type)
|
||||
throw new TypeError(`Expected typeof flag["${flagName}"].default === flag["${flagName}"].type, received "${typeof flagData.default}"!`);
|
||||
/** @type {JSTypes[]} */
|
||||
if(flagData.type === 'any' && !allowedTypeofDefaultType.includes(typeof flagData.default))
|
||||
throw new TypeError(`Expected typeof flag["${flagName}"].default === ${allowedTypeofDefaultType.map((type) => `"${type}"`).join(" | ")}, received "${typeof flagData.default}"!`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(typeof flagData.shorthands === 'string')
|
||||
flagData.shorthands = [flagData.shorthands];
|
||||
if(typeof flagData.shorthands === 'undefined')
|
||||
flagData.shorthands = [];
|
||||
if(!Array.isArray(flagData.shorthands) || flagData.shorthands.some((shorthand) => typeof shorthand !== 'string'))
|
||||
throw new TypeError(`flag["${flagName}"].shorthands must be type string | string[] | undefined!`);
|
||||
|
||||
if(typeof flagData.aliases === 'undefined')
|
||||
flagData.aliases = [];
|
||||
if(typeof flagData.aliases !== 'undefined' && (!Array.isArray(flagData.aliases) || flagData.aliases.some((alias) => typeof alias !== 'string')))
|
||||
throw new TypeError(`flag["${flagName}"].aliases must be type string[]!`);
|
||||
|
||||
//#region I hate that, setting useless properties that echo a value that I should supposedly already have, but it would require a non trival refactoring in a lot of places so this is easier
|
||||
//its also wrongly typed because obviously flag.name doesn't actually exist outside of me parsing the options, so it 's type is actually in `ReturnType<typeof getFlag()>`
|
||||
//#endregion
|
||||
///@ts-expect-error
|
||||
if(typeof flagData.name !== 'undefined' && flagData.name !== flagName) {
|
||||
throw new TypeError(`internal flag["${flagName}"].name must be set to flagName:"${flagName}" or undefined!`);
|
||||
}
|
||||
///@ts-expect-error
|
||||
flagData.name = flagName;
|
||||
|
||||
|
||||
if(typeof flagData.requiredValue === 'undefined')
|
||||
flagData.requiredValue = false;
|
||||
if(typeof flagData.requiredValue !== 'boolean')
|
||||
throw new TypeError(`flag["${flagName}"].requiredValue must be type boolean | undefined!`);
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
//#region findFlagFunctions
|
||||
|
||||
/**
|
||||
* @param {DefaultParserOptsType} opts
|
||||
* @param {string} flagKeyInput
|
||||
* @param {boolean} [isShorthand=false]
|
||||
*/
|
||||
function hasFlag(opts, flagKeyInput, isShorthand) {
|
||||
if(typeof isShorthand !== 'boolean')
|
||||
isShorthand = false;
|
||||
|
||||
/** @type {string} @readonly */
|
||||
/// @ts-ignore
|
||||
let flagKey = flagKeyInput
|
||||
|
||||
const definedFlagKeys = [];
|
||||
Object.entries(opts.flags).forEach(([flagKey, flag]) => {
|
||||
if(isShorthand) {
|
||||
if(flag.shorthands.length >= 1)
|
||||
definedFlagKeys.push(...flag.shorthands);
|
||||
} else {
|
||||
definedFlagKeys.push(flagKey);
|
||||
if(flag.aliases.length >= 1)
|
||||
definedFlagKeys.push(...flag.aliases);
|
||||
}
|
||||
});
|
||||
|
||||
if(opts.lowerCaseFlags) {
|
||||
flagKey = flagKey.toLowerCase();
|
||||
definedFlagKeys.forEach((flagKey) => flagKey = flagKey.toLowerCase());
|
||||
}
|
||||
|
||||
if(definedFlagKeys.includes(flagKey))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DefaultParserOptsType} opts
|
||||
* @param {string} flagKeyInput
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasShorthandFlag(opts, flagKeyInput) {
|
||||
return hasFlag(opts, flagKeyInput, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DefaultParserOptsType} opts
|
||||
* @param {string} flagKeyInput
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasFullFlag(opts, flagKeyInput) {
|
||||
return hasFlag(opts, flagKeyInput, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DefaultParserOptsType} opts
|
||||
* @param {string} flagKeyInput
|
||||
* @param {boolean} [isShorthand=false]
|
||||
* @returns {undefined | (FlagAny & { name: string })}
|
||||
*/
|
||||
function getFlag(opts, flagKeyInput, isShorthand) {
|
||||
if(typeof isShorthand !== 'boolean')
|
||||
isShorthand = false;
|
||||
|
||||
/** @type {string} @readonly */
|
||||
let flagKey = flagKeyInput
|
||||
|
||||
/** @type {{ flagKey: string, ref: FlagAny }[]} */
|
||||
const definedFlagKeys = [];
|
||||
/** For correct priorities in cases of similar aliases or shorthands, ordering by definition order (or at least trying) */
|
||||
Object.entries(opts.flags).forEach(([flagKey, flag]) => {
|
||||
if(isShorthand) {
|
||||
///@ts-expect-error
|
||||
flag.shorthands.forEach((shorthand) => definedFlagKeys.push({ flagKey: shorthand, ref: flag }));
|
||||
} else {
|
||||
definedFlagKeys.push({ flagKey, ref: flag });
|
||||
flag.aliases.forEach((alias) => definedFlagKeys.push({ flagKey: alias, ref: flag }));
|
||||
}
|
||||
});
|
||||
|
||||
if(opts.lowerCaseFlags) {
|
||||
flagKey = flagKey.toLowerCase();
|
||||
definedFlagKeys.forEach((definedFlagKeyRef) => definedFlagKeyRef.flagKey = definedFlagKeyRef.flagKey.toLowerCase());
|
||||
}
|
||||
|
||||
const foundFlag = definedFlagKeys.find((definedFlagKeyRef) => definedFlagKeyRef.flagKey === flagKey);
|
||||
|
||||
if(foundFlag !== undefined)
|
||||
/// @ts-ignore The type is correct
|
||||
return foundFlag.ref;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DefaultParserOptsType} opts
|
||||
* @param {string} flagKeyInput
|
||||
* @returns {ReturnType<typeof getFlag>}
|
||||
*/
|
||||
function getFullFlag(opts, flagKeyInput) {
|
||||
return getFlag(opts, flagKeyInput, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DefaultParserOptsType} opts
|
||||
* @param {string} flagKeyInput
|
||||
* @returns {ReturnType<typeof getFlag>}
|
||||
*/
|
||||
function getShorthandFlag(opts, flagKeyInput) {
|
||||
return getFlag(opts, flagKeyInput, true);
|
||||
}
|
||||
|
||||
//#endregion findFlagFunctions
|
||||
//#region end findFlagFunctions
|
||||
//#endregion end findFlagFunctions
|
||||
|
||||
/**
|
||||
* @related runFlagValueTypeResolving
|
||||
* @param {string} input
|
||||
* @returns {"number"|"boolean"|"string"}
|
||||
*/
|
||||
function getCastableType(input) {
|
||||
if(input.toLowerCase() === 'true' || input.toLowerCase() === 'false')
|
||||
return 'boolean';
|
||||
/// @ts-ignore
|
||||
if(!isNaN(input))
|
||||
return 'number';
|
||||
return 'string';
|
||||
}
|
||||
|
||||
class InputError extends Error {
|
||||
context = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {DefaultParserOptsType} opts
|
||||
* @param {{ error: InputError | Error, message: string, exitCode: number }} errorObj
|
||||
*/
|
||||
function giveError(opts, errorObj) {
|
||||
if(opts.behaviorOnInputError === 'throw') {
|
||||
throw errorObj.error;
|
||||
} else if(opts.behaviorOnInputError === 'log' || opts.behaviorOnInputError === 'log&exit') {
|
||||
opts.warningLogger(errorObj.message);
|
||||
}
|
||||
if(opts.behaviorOnInputError === 'log&exit') {
|
||||
process.exit(errorObj.exitCode || 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws if otps.behaviorOnInputError == 'throw' and flag value isn't assignable to it's required type
|
||||
* @param {DefaultParserOptsType} opts
|
||||
* @param {ReturnType<typeof argMetaCreator>} meta
|
||||
* @param {InternalFlagsFlagObj} flag
|
||||
* @param {string[]} argv - For the error context
|
||||
* @returns {boolean} - true = the assigned value is assignable to flag
|
||||
*/
|
||||
function isAssignedValueTypeAssignable(opts, meta, flag, argv) {
|
||||
/** @readonly */
|
||||
let flagConfig = flag.flagConfig
|
||||
if(!flag.lookedupFlagConfig) {
|
||||
if(flag.isShorthand)
|
||||
flagConfig = getShorthandFlag(opts, flag.key);
|
||||
else
|
||||
flagConfig = getFullFlag(opts, flag.key);
|
||||
flag.flagConfig = flagConfig;
|
||||
flag.lookedupFlagConfig = true;
|
||||
}
|
||||
|
||||
const castableType = getCastableType(meta.flagAssignedValue);
|
||||
if(flagConfig !== undefined && flagConfig.type !== 'any' && flagConfig.type !== castableType) {
|
||||
const message = `Value "${meta.flagAssignedValue}" (type:"${castableType}") is not assignable to required type "${flagConfig.type}".`;
|
||||
const error = new InputError(message);
|
||||
error.context = { flag: flagConfig, flagType: flagConfig.type, flagMatch: meta.flagKey, value: meta.flagAssignedValue, assignableType: castableType, argv };
|
||||
giveError(opts, {
|
||||
error,
|
||||
message: message + `\n - Flag: "${flagConfig.name}", match: "${meta.flagKey}", value: "${meta.flagAssignedValue}", assignableType: "${castableType}", requiredType: "${flagConfig.type}"`,
|
||||
exitCode: 1
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws if behaviorOnInputError == 'throw' and opts.allowUnknownFlags == false and flag isn't found in the flag config
|
||||
* @param {DefaultParserOptsType} opts
|
||||
* @param {string} flagKey
|
||||
* @param {any} flagVal
|
||||
* @param {string[]} argv - For the errror context
|
||||
*/
|
||||
function unknownFlag(opts, flagKey, flagVal, argv) {
|
||||
if(!opts.allowUnknownFlags) {
|
||||
const message = `Received unknown flag "${flagKey}", but opts.allowUnknownFlags is disabled!`;
|
||||
const error = new InputError(message);
|
||||
error.context = { flagKey, flagVal, argv };
|
||||
giveError(opts, {
|
||||
error,
|
||||
message: message + `\n - Flag: <not found>, match: "${flagKey}", value: "${flagVal}"`,
|
||||
exitCode: 2
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//#region SubworkFunctions
|
||||
|
||||
/**
|
||||
* @param {DefaultParserOptsType} opts
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function getAllShorthandFlags(opts) {
|
||||
const allShorthandFlags = []
|
||||
Object.values(opts.flags).forEach((flag) => {
|
||||
allShorthandFlags.push(...flag.shorthands);
|
||||
/** FLAG: ${opts.automaticBooleanFlagNegation.shorthandNegation} */
|
||||
if(opts.automaticBooleanFlagNegation.shorthandNegation && flag.type === 'boolean') {
|
||||
/// @ts-expect-error
|
||||
allShorthandFlags.push(...flag.shorthands.map((shorthand) => "n" + shorthand));
|
||||
}
|
||||
});
|
||||
return allShorthandFlags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DefaultParserOptsType} opts
|
||||
* @param {string} flagKey
|
||||
* @param {any} flagVal
|
||||
*/
|
||||
function runFullFlagAutomaticBooleanNegation(opts, flagKey, flagVal) {
|
||||
/** Make sure flag isn't set in our vals */
|
||||
if(!hasFullFlag(opts, flagKey) && opts.automaticBooleanFlagNegation.enabled && typeof flagVal === 'boolean' && flagKey.length > 2) {
|
||||
if(flagKey.length > 3 && flagKey.toLowerCase().startsWith('no-')) {
|
||||
flagKey = flagKey.slice(3);
|
||||
flagVal = !flagVal;
|
||||
}
|
||||
else if(opts.automaticBooleanFlagNegation.allowUnspacedNegatedFlags && flagKey.toLowerCase().startsWith('no')) {
|
||||
flagKey = flagKey.slice(2);
|
||||
flagVal = !flagVal;
|
||||
}
|
||||
}
|
||||
return { flagKey, flagVal }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DefaultParserOptsType} opts
|
||||
* @param {string} flagKey
|
||||
* @param {any} flagVal
|
||||
*/
|
||||
function runShorthandFlagAutomaticBooleanNegation(opts, flagKey, flagVal) {
|
||||
/** Make sure flag isn't set in our vals */
|
||||
if(!hasFullFlag(opts, flagKey) && opts.automaticBooleanFlagNegation.enabled && opts.automaticBooleanFlagNegation.shorthandNegation && typeof flagVal === 'boolean' && flagKey.length >= 2) {
|
||||
if(opts.automaticBooleanFlagNegation.allowUnspacedNegatedFlags && flagKey.toLowerCase().startsWith('n')) {
|
||||
flagKey = flagKey.slice(1);
|
||||
flagVal = !flagVal;
|
||||
}
|
||||
}
|
||||
return { flagKey, flagVal }
|
||||
}
|
||||
|
||||
/**
|
||||
* @related getCastableType
|
||||
* @param {DefaultParserOptsType} opts
|
||||
* @param {string} flagKey
|
||||
* @param {any} flagVal
|
||||
*/
|
||||
function runFlagValueTypeResolving(opts, flagKey, flagVal) {
|
||||
/** boolean checking */
|
||||
if(flagVal.toLowerCase() === 'true')
|
||||
flagVal = true;
|
||||
else if(flagVal.toLowerCase() === 'false')
|
||||
flagVal = false;
|
||||
else if(opts.resolveFlagValueTypes) {
|
||||
/** number checking */
|
||||
/// @ts-ignore
|
||||
if(!isNaN(flagVal))
|
||||
flagVal = Number(flagVal);
|
||||
}
|
||||
return { flagVal };
|
||||
}
|
||||
|
||||
//#endregion SubworkFunctions
|
||||
//#region SecondaryParsers
|
||||
|
||||
/**
|
||||
* @param {string} arg
|
||||
*/
|
||||
function argMetaCreator(arg) {
|
||||
const argsStr = new StringConsumer(arg);
|
||||
|
||||
/** return meta flags */
|
||||
let isFlag = false;
|
||||
/** @type {string} */
|
||||
let flagKey = null;
|
||||
let minusSignCount = 0;
|
||||
|
||||
let unescapedEqualSignPos = -1;
|
||||
let hasFlagAssignedValue = false;
|
||||
//TODO: Add support for comma separated args into arr for flag values
|
||||
/** @type {string} */
|
||||
let flagAssignedValue = null;
|
||||
|
||||
|
||||
/** parser character flags */
|
||||
let isBackslashed = false;
|
||||
let isPotentialFlag = false;
|
||||
let hasPotentialFlagAssignedValue = false;
|
||||
let readingMinusSignCount = true;
|
||||
while(argsStr.hasNext()) {
|
||||
let char = argsStr.next();
|
||||
/** !isBackslashed check redundant here, readingMinusSignCount will always be false if previous character was not a - */
|
||||
if(char === '-' && readingMinusSignCount) {
|
||||
isPotentialFlag = true;
|
||||
minusSignCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
/** Must not be a minus sign */
|
||||
readingMinusSignCount = false;
|
||||
|
||||
/** Flag has an assigned value in the arg*/
|
||||
if(char === "=" && isFlag && !isBackslashed && !hasPotentialFlagAssignedValue) {
|
||||
hasPotentialFlagAssignedValue = true;
|
||||
unescapedEqualSignPos = argsStr.getPointer()-1;
|
||||
continue;
|
||||
}
|
||||
|
||||
/** Making sure there is indeed something after the initial - */
|
||||
if(isPotentialFlag && !isFlag) {
|
||||
isFlag = true;
|
||||
flagKey = '';
|
||||
}
|
||||
|
||||
/** Making sure there is indeed something after the = */
|
||||
if(hasPotentialFlagAssignedValue && !hasFlagAssignedValue) {
|
||||
hasFlagAssignedValue = true;
|
||||
flagAssignedValue = '';
|
||||
}
|
||||
|
||||
if(isFlag) {
|
||||
/** If is still during flag phase, add flagKey */
|
||||
if(!hasFlagAssignedValue) {
|
||||
flagKey += char;
|
||||
}
|
||||
/** Else if it has a flagAssignedValue, add value */
|
||||
else {
|
||||
flagAssignedValue += char;
|
||||
}
|
||||
}
|
||||
|
||||
// Idk how to feel about the partial backslashes handling here, most other flag parsers don't handle it but it seems mostly logical to me
|
||||
if(char === '\\' && !isBackslashed)
|
||||
isBackslashed = true;
|
||||
else if(isBackslashed)
|
||||
isBackslashed = false;
|
||||
}
|
||||
return { isFlag, minusSignCount, flagKey, hasFlagAssignedValue, flagAssignedValue, unescapedEqualSignPos };
|
||||
}
|
||||
|
||||
// TODO: Integrate this function in the main argMetaCreator function, redundant while loop;
|
||||
/**
|
||||
* @param {string[]} shorthandFlags
|
||||
* @param {string} shorthandString
|
||||
*/
|
||||
function parseShorthandFlags(shorthandFlags, shorthandString) {
|
||||
shorthandFlags = shorthandFlags.concat();
|
||||
const shorthandStr = new StringConsumer(shorthandString);
|
||||
|
||||
let foundFlags = [];
|
||||
let currStr = '';
|
||||
|
||||
while(shorthandStr.hasNext()) {
|
||||
let char = shorthandStr.next();
|
||||
currStr += char;
|
||||
|
||||
let perfectMath = shorthandFlags.findIndex((shorthand) => shorthand === currStr);
|
||||
let lastIsmMatch = shorthandFlags.findIndex((shorthand) => shorthand === currStr.slice(0, -1));
|
||||
|
||||
/** Has shorthand that can start with the currentStr but is missing some letters */
|
||||
if(shorthandStr.hasNext() && shorthandFlags.some((shorthand) => shorthand.startsWith(currStr) && currStr !== shorthand)) {
|
||||
continue;
|
||||
} else if(perfectMath !== -1) {
|
||||
foundFlags.push(currStr);
|
||||
shorthandFlags.splice(perfectMath, 1);
|
||||
} else if(lastIsmMatch !== -1) {
|
||||
foundFlags.push(currStr.slice(0, -1));
|
||||
shorthandFlags.splice(lastIsmMatch, 1);
|
||||
if(shorthandStr.hasNext()) {
|
||||
currStr = char;
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
foundFlags.push(char);
|
||||
}
|
||||
} else {
|
||||
foundFlags.push(...currStr.split(''))
|
||||
}
|
||||
|
||||
currStr = '';
|
||||
}
|
||||
|
||||
return foundFlags;
|
||||
}
|
||||
|
||||
//#endregion SecondaryParsers
|
||||
//#region end SecondaryParsers
|
||||
//#endregion end SecondaryParsers
|
||||
|
||||
class UnsetFlagValueType {}
|
||||
const UNSET_FLAG_VALUE = new UnsetFlagValueType();
|
||||
|
||||
/**
|
||||
* @interface
|
||||
* @typedef {{ key: string, value: typeof UNSET_FLAG_VALUE | number | boolean | string, isShorthand: boolean, lookedupFlagConfig: boolean, flagConfig: ReturnType<typeof getFlag> }} InternalFlagsFlagObj
|
||||
*/
|
||||
|
||||
// TODO: Support multiple flag as a option = overload input
|
||||
// TODO: ^ Add a flag so that if the previous thing is turned off, determine which flag gets picked first (last or first instance, current=last)
|
||||
// TODO: Export a function to list all the flags and their values, with a description property on the flags
|
||||
|
||||
// TODO: Add an option for custom flag setting character, and wether they're stackable or not (so something to allow '/flag' to work)
|
||||
|
||||
// TODO: Fix typescript types of returned flags to include opts.resolveFlagValueTypes into it as well as checking if it has a default and if not make it an optional?, and type never if it's not in the flags object
|
||||
/**
|
||||
* @template {FlagsI} Flags
|
||||
* @template {{ [K in keyof Flags]: Flags[K]['type'] extends 'boolean' ? boolean : Flags[K]['type'] extends 'number' ? number : Flags[K]['type'] extends 'string' ? string : string | boolean | number }} FlagsReturn
|
||||
* @param {string[]} argv
|
||||
* @param {ParserOpts<Flags>} opts
|
||||
* @returns {{ input: string[], flags: FlagsReturn } }}
|
||||
*/
|
||||
function parser(argv, opts) {
|
||||
if(!Array.isArray(argv) || argv.some((e) => typeof e !== 'string'))
|
||||
throw new TypeError(`Argv must be type string[]!`);
|
||||
|
||||
expectType('opts', 'object', opts, false, false);
|
||||
/** @throwable Will throw if anything is wrong */
|
||||
validateAndFillDefaults(opts);
|
||||
|
||||
const allShorthandFlags = getAllShorthandFlags(opts);
|
||||
|
||||
/** @type {InternalFlagsFlagObj[]} */
|
||||
const flags = [];
|
||||
/** @type {string[]} */
|
||||
const input = [];
|
||||
|
||||
let lookingForFlagVal = false;
|
||||
argv.forEach((arg) => {
|
||||
const meta = argMetaCreator(arg);
|
||||
if(meta.isFlag) {
|
||||
/** If allow singular dashlong flags, just fall through to the full flag handling */
|
||||
/** Redundant hasFullFlag given later might getFullFlag but yk easier implementation like this */
|
||||
if(meta.minusSignCount === 1 && (!opts.allowSingularDashLongFlags || !hasFullFlag(opts, meta.flagKey))) {
|
||||
if(opts.lowerCaseFlags)
|
||||
arg = arg.toLowerCase();
|
||||
|
||||
parseShorthandFlags(allShorthandFlags, meta.flagKey)
|
||||
.forEach((arg) => flags.push({ key: arg, value: UNSET_FLAG_VALUE, isShorthand: true, lookedupFlagConfig: false, flagConfig: null }));
|
||||
|
||||
if(meta.hasFlagAssignedValue) {
|
||||
const flag = flags.at(-1);
|
||||
/** @throwable Can throw depending on opts.behaviorOnInputError */
|
||||
if(isAssignedValueTypeAssignable(opts, meta, flag, argv))
|
||||
flag.value = meta.flagAssignedValue;
|
||||
}
|
||||
else {
|
||||
lookingForFlagVal = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/** @readonly I like dark blue, plus if is better than a ? assignment from a const */
|
||||
let flagKey = meta.flagKey;
|
||||
if(opts.lowerCaseFlags)
|
||||
flagKey = flagKey.toLowerCase();
|
||||
|
||||
const flagConfig = getFullFlag(opts, flagKey);
|
||||
|
||||
if(meta.hasFlagAssignedValue) {
|
||||
/** @type {InternalFlagsFlagObj} */
|
||||
const flag = { key: flagKey, value: meta.flagAssignedValue, isShorthand: false, lookedupFlagConfig: true, flagConfig: flagConfig };
|
||||
if(!isAssignedValueTypeAssignable(opts, meta, flag, argv))
|
||||
return;
|
||||
|
||||
flags.push(flag);
|
||||
lookingForFlagVal = false;
|
||||
return;
|
||||
}
|
||||
|
||||
flags.push({ key: flagKey, value: UNSET_FLAG_VALUE, isShorthand: false, lookedupFlagConfig: true, flagConfig });
|
||||
lookingForFlagVal = true;
|
||||
return;
|
||||
|
||||
}
|
||||
else if (lookingForFlagVal) {
|
||||
const flag = flags.at(-1);
|
||||
/** @readonly */
|
||||
let flagConfig = flag.flagConfig
|
||||
if(!flag.lookedupFlagConfig) {
|
||||
if(flag.isShorthand)
|
||||
flagConfig = getShorthandFlag(opts, flag.key);
|
||||
else
|
||||
flagConfig = getFullFlag(opts, flag.key);
|
||||
flag.flagConfig = flagConfig;
|
||||
flag.lookedupFlagConfig = true;
|
||||
}
|
||||
|
||||
|
||||
let flagType = opts.defaultFlagType;
|
||||
if(flagConfig !== undefined && flagConfig.type !== undefined)
|
||||
flagType = flagConfig.type;
|
||||
|
||||
const { flagVal } = runFlagValueTypeResolving(opts, flag.key, arg);
|
||||
|
||||
if(flagType === typeof flagVal || flagType === 'any')
|
||||
flag.value = arg;
|
||||
else
|
||||
input.push(arg);
|
||||
lookingForFlagVal = false;
|
||||
return;
|
||||
}
|
||||
else {
|
||||
input.push(arg);
|
||||
return;
|
||||
}
|
||||
})
|
||||
|
||||
const flagReturnObj = {};
|
||||
/** Some (not all, some is run in previous loop) Flag processing (like checking types, which we can't run in the argv.forEach) and return object building */
|
||||
flags.forEach((flag) => {
|
||||
/** Process some flag values */
|
||||
let { key: flagKey, value: flagVal } = flag;
|
||||
|
||||
/** Building return OBJ */
|
||||
/** @readonly */
|
||||
let flagConfig = flag.flagConfig
|
||||
if(!flag.lookedupFlagConfig) {
|
||||
if(flag.isShorthand)
|
||||
flagConfig = getShorthandFlag(opts, flag.key);
|
||||
else
|
||||
flagConfig = getFullFlag(opts, flag.key);
|
||||
}
|
||||
ok(flagConfig !== null, 'flagConfig should always be undefined if not found, and would only be in the wrong state of null if not reassigned at a later point (which should be impossible');
|
||||
|
||||
if(flagVal === UNSET_FLAG_VALUE) {
|
||||
/** Setting defaults */
|
||||
/** @type {number|string|boolean} */
|
||||
flagVal = true;
|
||||
if(flagConfig !== undefined) {
|
||||
if(flagConfig.requiredValue === true) {
|
||||
const message = `Flag "${flagConfig.name}" is missing a required value of type "${flagConfig.type}"!`;
|
||||
const error = new InputError(message);
|
||||
error.context = { flag: flagConfig, flagType: flagConfig.type, flagMatch: flagKey, value: UNSET_FLAG_VALUE, argv };
|
||||
return giveError(opts, {
|
||||
error,
|
||||
message: message + `\n - Flag: "${flagConfig.name}", match: "${flagKey}", value: UNSET_FLAG_VALUE, type: "${flagConfig.type}"`,
|
||||
exitCode: 1
|
||||
});
|
||||
}
|
||||
/** Set default unless bool type */
|
||||
if(typeof flagConfig.default !== 'undefined' && flagConfig.type !== 'boolean')
|
||||
flagVal = flagConfig.default;
|
||||
}
|
||||
}
|
||||
|
||||
if(typeof flagVal === 'string')
|
||||
({ flagVal } = runFlagValueTypeResolving(opts, flagKey, flagVal));
|
||||
/** Only on type flagVal */
|
||||
if(flag.isShorthand)
|
||||
({ flagKey, flagVal } = runShorthandFlagAutomaticBooleanNegation(opts, flagKey, flagVal));
|
||||
else
|
||||
({ flagKey, flagVal } = runFullFlagAutomaticBooleanNegation(opts, flagKey, flagVal));
|
||||
|
||||
|
||||
flag.key = flagKey;
|
||||
flag.value = flagVal;
|
||||
|
||||
let realFlagKey = flag.key;
|
||||
if(flagConfig !== undefined) {
|
||||
realFlagKey = flagConfig.name;
|
||||
}
|
||||
else
|
||||
unknownFlag(opts, flagKey, flagVal, argv);
|
||||
|
||||
flagReturnObj[realFlagKey] = flag.value;
|
||||
})
|
||||
|
||||
/** Setting defaults if missing */
|
||||
Object.entries(opts.flags).forEach(([flagName, flagConfig]) => {
|
||||
if(typeof flagReturnObj[flagName] !== 'undefined')
|
||||
return;
|
||||
/** Not a needed check as you can tell, but present to explicitely highlight that behavior in the code */
|
||||
if(typeof flagConfig.default === 'undefined')
|
||||
return flagReturnObj[flagName] = undefined;
|
||||
flagReturnObj[flagName] = flagConfig.default;
|
||||
});
|
||||
|
||||
/// @ts-ignore
|
||||
return { flags: flagReturnObj, input };
|
||||
}
|
||||
|
||||
module.exports = { parser };
|
||||
18
jsconfig.json
Normal file
18
jsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "nodenext",
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
// "jsx": "react",
|
||||
// "allowImportingTsExtensions": true,
|
||||
// "strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"checkJs": true,
|
||||
// "typeRoots": ["./types.d.ts"]
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/node_modules/*"
|
||||
]
|
||||
}
|
||||
33
package-lock.json
generated
Normal file
33
package-lock.json
generated
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "ox-flags-parser",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ox-flags-parser",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz",
|
||||
"integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
package.json
Normal file
15
package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "ox-flags-parser",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "@Oxtaly",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.3"
|
||||
}
|
||||
}
|
||||
66
types.d.ts
vendored
Normal file
66
types.d.ts
vendored
Normal file
@ -0,0 +1,66 @@
|
||||
|
||||
interface FlagT<T, Z> {
|
||||
default?: T,
|
||||
aliases?: string[],
|
||||
/** Shorhands, AKA only work with singular minus signs like -f; Multiple characters are allowed; Will not count as a full (like --f) flag alias; */
|
||||
shorthands?: string|string[],
|
||||
type?: Z
|
||||
/** Whether to send/throw an error if the flag is present but no value is set, either with an assigned (--flag=value) value, or regular --flag value @default false */
|
||||
requiredValue?: boolean
|
||||
};
|
||||
|
||||
export type FlagAny = FlagT<string, "string"> | FlagT<boolean, "boolean"> | FlagT<number, "number"> | FlagT<string | number | boolean, "any">
|
||||
|
||||
export interface FlagsI { [key: readonly string]: FlagAny }
|
||||
|
||||
export type DefaultParserOptsType = ParserOpts<FlagsI>
|
||||
|
||||
/** This object will be modified by parser */
|
||||
export interface ParserOpts<Flags extends FlagsI> {
|
||||
flags?: Flags,
|
||||
/** Wether to allow versions of `-flag` as a singular and valid flag. @default false */
|
||||
allowSingularDashLongFlags?: boolean = false
|
||||
/** Wether to allow flags not denoted in the flag property. @default true */
|
||||
allowUnknownFlags?: boolean = true
|
||||
/** Wether to lowercase all flags key. Incompatible with camelCaseFlags option. @default true */
|
||||
lowerCaseFlags?: boolean = true
|
||||
/** Wether to automatically resolve flag number values into numbers and boolean flags into booleans if flag type is explicitly set. @default true */
|
||||
resolveFlagValueTypes?: boolean = true
|
||||
/**
|
||||
* Setting according flag to the inversed provided boolean value (defaulting to false if none is provided) with their '--no' variants unless a defined flag exists with that
|
||||
* name already; Additional informations in sub-properties.
|
||||
* @default
|
||||
* ```js
|
||||
* = { enabled: true, allowUnspacedNegatedFlags: true, shorthandNegation: true }
|
||||
* ```
|
||||
*/
|
||||
automaticBooleanFlagNegation?: {
|
||||
/** Wether to automatically resolve boolean flags that are negated to the inverse boolean value. Requires resolveFlagValueTypes. @default true */
|
||||
enabled?: boolean;
|
||||
/** Allows flags negated without a space (eg: "--noflag") to still be automatically resolved @default true */
|
||||
allowUnspacedNegatedFlags?: boolean;
|
||||
/** Wether to negate shorthand flags with if an n is present before hand (eg: '-nf'). Providing a shorthand of 'n' will disable this feature. @default true */
|
||||
shorthandNegation?: boolean;
|
||||
}
|
||||
/** Enable this option to silence any warnings about potentially unexpected behavior (eg: a flag's unset type being set to the default). @default false */
|
||||
silenceWarnings?: boolean;
|
||||
/** Override to change how warnings are emitted @default console.warn */
|
||||
warningLogger?: (log: string) => void;
|
||||
/** Default type for either unknown flags or flags without an explicit type being set @default "any" */
|
||||
defaultFlagType?: "string" | "boolean" | "number" | "any";
|
||||
/**
|
||||
* Behavior when input does not follow the provided flags constraints (eg: flag assigned value (--flag=value) not being the correct type).
|
||||
*
|
||||
* Setting it to 'ignore' or 'log' is highly not recommended and will lead to undefined behavior. (eg: flags not meeting the configuration not being present
|
||||
* in the final returned object)
|
||||
* Setting this to 'ignore' will give the same undefined behavior as setting it to 'log', but without emitting any logs to `warningLogger`.
|
||||
* @default 'throw'
|
||||
*/
|
||||
behaviorOnInputError?: 'throw' | 'log' | 'log&exit' | 'ignore';
|
||||
/** Wether to allow default flag values set to null being valid @default true */
|
||||
allowNullDefaultFlagValues?: boolean;
|
||||
}
|
||||
|
||||
function __type__getType(e: any) { return typeof e };
|
||||
|
||||
export type JSTypes = ReturnType<typeof __type__getType>
|
||||
Loading…
x
Reference in New Issue
Block a user