ox-argv-parser/index.js

851 lines
32 KiB
JavaScript

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
*/
class InputError extends Error {
context = null;
};
class UnsetFlagValueType {}
const UNSET_FLAG_VALUE = new UnsetFlagValueType();
/**
* @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", "bigint", "boolean"];
const validFlagTypes = ['any', ...allowedTypeofDefaultType];
const allowedBehaviorOnInputErrorTypes = ['throw', 'log', 'log&exit', 'ignore'];
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);
validateOrDefaultIfUnset(opts, 'lowerCaseFlagValues', 'boolean', false);
validateOrDefaultIfUnset(opts, 'lowerCaseInputValues', '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"|"bigint"|"boolean"|"string"}
*/
function getCastableType(input) {
if(input.toLowerCase() === 'true' || input.toLowerCase() === 'false')
return 'boolean';
/// @ts-ignore
if(input.length >= 2 && input.endsWith('n') && !isNaN(input.slice(0, -1)))
return 'bigint';
/// @ts-ignore
if(!isNaN(input))
return 'number';
return 'string';
}
/**
* @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 !== 'string' && flagConfig.type !== castableType) {
if(flagConfig.type === 'bigint' && castableType === 'number')
return true;
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;
}
/**
* @modifies flag
* ### To be run before any flag value assignments (flag.value always expected to be string)
* @param {DefaultParserOptsType} opts
* @param {InternalFlagsFlagObj} flag
*/
function runAutomaticBooleanFlagNegation(opts, flag) {
if(opts.automaticBooleanFlagNegation.enabled !== true)
return false;
if(flag.isShorthand && opts.automaticBooleanFlagNegation.shorthandNegation !== true)
return false;
/** @readonly */
let newKey = null;
/** @readonly */
let flagConfig = null;
let negated = false;
if(!flag.isShorthand) {
/** Make sure flag isn't set in our vals */
if(!hasFullFlag(opts, flag.key) && flag.key.length > 2) {
if(flag.key.length > 3 && flag.key.toLowerCase().startsWith('no-')) {
newKey = flag.key.slice(3);
negated = true;
flagConfig = getFullFlag(opts, newKey);
}
else if(opts.automaticBooleanFlagNegation.allowUnspacedNegatedFlags && flag.key.toLowerCase().startsWith('no')) {
newKey = flag.key.slice(2);
negated = true;
flagConfig = getFullFlag(opts, newKey);
}
}
} else {
/** Make sure flag isn't set in our vals */
if(!hasShorthandFlag(opts, flag.key) && flag.key.length >= 2) {
if(flag.key.toLowerCase().startsWith('n')) {
newKey = flag.key.slice(1);
negated = true;
flagConfig = getShorthandFlag(opts, newKey);
}
}
}
if(negated) {
/** @readonly */
let flagType = opts.defaultFlagType;
if(typeof flagConfig !== 'undefined')
flagType = flagConfig.type;
/** @type {ReturnType<typeof getCastableType>} */
let castableType = 'boolean';
if(flag.value !== UNSET_FLAG_VALUE)
/// @ts-ignore
castableType = getCastableType(flag.value);
if((flagType === 'boolean' || flagType === 'any') && castableType === 'boolean') {
flag.key = newKey;
flag.flagConfig = flagConfig;
flag.lookedupFlagConfig = true;
if(flag.value === UNSET_FLAG_VALUE)
flag.value = false;
else {
/** @type {string} */
/// @ts-ignore
let flagVal = flag.value;
let boolVal = runFlagValueTypeResolving(opts, flagVal, undefined);
flag.value = !boolVal;
}
return true;
}
}
return false;
}
/**
* @related getCastableType
* ### Warning: This does not check the flag type despite passing in a flagConfig!
* @param {DefaultParserOptsType} opts
* @param {string} flagVal
* @param {ReturnType<typeof getFlag>} flagConfig
*/
function runFlagValueTypeResolving(opts, flagVal, flagConfig) {
/** @type {any} */
let resolvedFlagVal = flagVal;
/** boolean checking */
if(flagVal.toLowerCase() === 'true')
resolvedFlagVal = true;
else if(flagVal.toLowerCase() === 'false')
resolvedFlagVal = false;
else if(opts.resolveFlagValueTypes) {
/** bigint checking */
/// @ts-ignore
if(flagVal.length >= 2 && flagVal.endsWith('n') && !isNaN(flagVal.slice(0, -1)))
resolvedFlagVal = BigInt(flagVal.slice(0, -1));
/** number & bigint checking */
/// @ts-ignore
else if(!isNaN(flagVal)) {
if(typeof flagConfig === 'undefined' || flagConfig.type === 'number' || flagConfig.type === 'any')
resolvedFlagVal = Number(flagVal);
else if (typeof flagConfig !== 'undefined' && flagConfig.type === 'bigint')
resolvedFlagVal = BigInt(flagVal);
}
}
return resolvedFlagVal;
}
//#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
/**
* @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 castableType = getCastableType(arg);
if(['string', 'any', castableType].includes(flagType) || (castableType === 'number' && flagType === 'bigint'))
flag.value = arg;
else {
if(opts.lowerCaseInputValues)
input.push(arg.toLowerCase());
else
input.push(arg);
}
lookingForFlagVal = false;
return;
}
else {
if(opts.lowerCaseInputValues)
input.push(arg.toLowerCase());
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) => {
/** Only on type flagVal */
runAutomaticBooleanFlagNegation(opts, 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(flag.value === UNSET_FLAG_VALUE) {
let flagType = opts.defaultFlagType
if(flagConfig !== undefined) {
flagType = flagConfig.type;
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: flag.key, value: UNSET_FLAG_VALUE, argv };
return giveError(opts, {
error,
message: message + `\n - Flag: "${flagConfig.name}", match: "${flag.key}", value: UNSET_FLAG_VALUE, type: "${flagConfig.type}"`,
exitCode: 1
});
}
/** Set default unless bool type */
if(typeof flagConfig.default !== 'undefined' && flagConfig.type !== 'boolean')
flag.value = flagConfig.default;
}
if(flag.value === UNSET_FLAG_VALUE) {
switch(flagType) {
case 'any':
case 'boolean':
flag.value = true;
break;
case "bigint":
case "number":
case "string":
flag.value = null;
break;
}
}
}
if(typeof flag.value === 'string')
flag.value = runFlagValueTypeResolving(opts, flag.value, flagConfig);
if(typeof flag.value === 'string' && opts.lowerCaseFlagValues)
flag.value = flag.value.toLowerCase()
let realFlagKey = flag.key;
if(flagConfig !== undefined) {
realFlagKey = flagConfig.name;
}
else
unknownFlag(opts, flag.key, flag.value, argv);
flagReturnObj[realFlagKey] = flag.value;
})
/** Setting missing flags */
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 };