const { StringConsumer } = require('../OxLib/utils/utils') const { ok } = require('node:assert'); /** * @interface * @template {FlagsI} Flags * @typedef {import('./types.d.ts').ParserOpts} ParserOpts */ /** * @typedef {import('./types.d.ts').DefaultParserOptsType} DefaultParserOptsType */ /** * @template T * @template Z * @typedef {import('./types.d.ts').FlagT} 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, 'handleBackslashesForSpecialFlagChars', '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` //#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} */ function getFullFlag(opts, flagKeyInput) { return getFlag(opts, flagKeyInput, false); } /** * @param {DefaultParserOptsType} opts * @param {string} flagKeyInput * @returns {ReturnType} */ 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} 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: , 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} */ 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} 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 * @param {DefaultParserOptsType} opts */ function argMetaCreator(arg, opts) { 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) { if(!isBackslashed) { if(!hasPotentialFlagAssignedValue) { hasPotentialFlagAssignedValue = true; unescapedEqualSignPos = argsStr.getPointer()-1; continue; } } else { if(hasFlagAssignedValue) { /** Consume the backslash */ flagAssignedValue = flagAssignedValue.slice(0, -1) } else { /** Consume the backslash */ flagKey = flagKey.slice(0, -1) } isBackslashed = false; } } /** 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; } } // Weird and gross handling of backslashes, but explicit, 'defined' (in ./types.d.ts -> ParserOpts.handleBackslashesForSpecialFlagChars) and toggleable if(char === '\\') { if(opts.handleBackslashesForSpecialFlagChars) { if(!isBackslashed) { if(isFlag && argsStr.hasNext() && argsStr.peek() === '-') { /** Consume the backslash */ flagKey = flagKey.slice(0, -1) } else isBackslashed = true; } } } else if(isBackslashed) isBackslashed = false; } return { isFlag, minusSignCount, flagKey, hasFlagAssignedValue, flagAssignedValue, unescapedEqualSignPos }; } // // * Impossibel to easily integrate due to backslash lookahead handling for the flag key // // ^ ~~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 }} 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} 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, opts); 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 { let inputVal = arg; // Hardcoded edge case, not a fan, but if it has more than one backslash, it was never gonna be a flag anyways if(opts.handleBackslashesForSpecialFlagChars && inputVal.startsWith('\\-')) inputVal = inputVal.slice(1); if(!lookingForFlagVal) { if(opts.lowerCaseInputValues) inputVal = inputVal.toLowerCase(); input.push(inputVal); return } 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(inputVal); if(['string', 'any', castableType].includes(flagType) || (castableType === 'number' && flagType === 'bigint')) flag.value = inputVal; else { if(opts.lowerCaseInputValues) inputVal = inputVal.toLowerCase(); input.push(inputVal); } lookingForFlagVal = false; 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 };