// ox-argv-wrapper // Copyright (C) 2025 Oxtaly // // This program is free software: you can redistribute it and/or modify // // it under the terms of the GNU General Public License as published by // // the Free Software Foundation, either version 3 of the License, or // // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // // but WITHOUT ANY WARRANTY; without even the implied warranty of // // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // // along with this program. If not, see . /** 'bundling' my own dependencies */ class StringConsumer { /** @readonly */ static VERSION = 3; /** * @param {string} str */ constructor(str) { /** @private */ this.base = str; /** @private */ this.i = 0; } /** * Returns the index of the next character in line * If getPointer() == getFullStr().length, hasNext() = false */ getPointer() { return this.i; } getFullStr() { return this.base; } /** * Get the next characters while consuming them * @param {number} [num] */ next(num) { if(num < 1) throw new RangeError(`Num ${num} < 1! Num must be between 1 and fullStr length.`) if(typeof num !== 'number') num = 1; if(this.i == this.base.length) return null; let retStr = this.base.slice(this.i, this.i + num); this.i += num; if(this.i >= this.base.length) this.i = this.base.length; return retStr; } /** * Get the next characters without consuming them * @param {number} [num] */ peek(num) { if(num < 1) throw new RangeError(`Num ${num} < 1! Num must be between 1 and fullStr length.`) if(typeof num !== 'number') num = 1; if(this.i == this.base.length) return null; let retStr = this.base.slice(this.i, this.i + num); return retStr; } reset() { this.i = 0; } end() { return this.next(this.base.length-this.i); } /** * @param {number} [num] */ hasNext(num) { if(num < 1) throw new RangeError(`Num ${num} < 1! Num must be between 1 and fullStr length.`) if(typeof num !== 'number') num = 1; return this.i + num <= this.base.length; } } 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 */ /** * @typedef {import('./types.d.ts').argMeta} argMeta */ 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] */ function expectType(key, type, value, acceptsUndefined) { if(typeof acceptsUndefined !== 'boolean') acceptsUndefined = false; if(typeof value === 'undefined') { if(acceptsUndefined) return else throw new TypeError(`Required type ${type} for ${key} is undefined!`); } 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 = 'opts'; if(typeof obj[keyName] === 'undefined') return obj[keyName] = defaultVal; return expectType(`${objName}.${String(keyName)}`, keyType, obj[keyName], true) } /** * @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) { return 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(" | ")} | undefined, received "${opts.behaviorOnInputError}"!`); validateOrDefaultIfUnset(opts, 'defaultFlagType', 'string', 'any'); if(!validFlagTypes.includes(opts.defaultFlagType)) throw new TypeError(`Expected opts.defaultFlagType === ${validFlagTypes.map((type) => `"${type}"`).join(" | ")} | undefined, received "${opts.defaultFlagType}"!`); /** Category: @default false */ validateOrDefaultIfUnset(opts, 'allowSingularDashLongFlags', 'boolean', false); validateOrDefaultIfUnset(opts, 'lowerCaseFlagValues', 'boolean', false); validateOrDefaultIfUnset(opts, 'lowerCaseInputValues', 'boolean', false); if(opts.shouldStopParsingFunc !== null) validateOrDefaultIfUnset(opts, 'shouldStopParsingFunc', 'function', null); if(opts.shouldStopParsingStr !== null) validateOrDefaultIfUnset(opts, 'shouldStopParsingStr', 'string', null); if(opts.shouldStopParsingStr !== null && opts.shouldStopParsingFunc !== null) { opts.warningLogger(`warn: opts.shouldStopParsingStr and opts.shouldStopParsingFunc are both present, opts.shouldStopParsingStr will be ignored!`); opts.shouldStopParsingStr = null; } if(opts.parseFilterFunc !== null) validateOrDefaultIfUnset(opts, 'parseFilterFunc', 'function', null); /** 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, 'addFlagShorthandsAsAliases', '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); 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 trivial 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!`); if(typeof flagData.acceptsNaturalValue === 'undefined') flagData.acceptsNaturalValue = true; if(typeof flagData.acceptsNaturalValue !== 'boolean') throw new TypeError(`flag["${flagName}"].acceptsNaturalValue 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); } /** * @param {DefaultParserOptsType} opts * @param {InternalFlagsFlagObj} flag */ function assureValidFlagConfigState(opts, flag) { if(flag.config === null) { if(flag.isShorthand) flag.config = getShorthandFlag(opts, flag.key); else flag.config = getFullFlag(opts, flag.key); } } //#endregion findFlagFunctions //#region end findFlagFunctions //#endregion end findFlagFunctions /** * @related runFlagValueTypeResolving,isTypeAssignable * @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'; } /** * @related runFlagValueTypeResolving,getCastableType * @param {FlagAny['type']} baseType * @param {FlagAny['type']} comparedType */ function isTypeAssignable(baseType, comparedType) { if(baseType === comparedType) return true; if(baseType === 'any' || baseType === 'string') return true; if(baseType === 'bigint' && comparedType === 'number') return true; return false } /** * @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 || 1); } } /** * @throws if opts.behaviorOnInputError == 'throw' && 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 */ function assignedValueUncastableTypeError(opts, meta, flag, argv) { const castableType = getCastableType(meta.flagAssignedValue); let valDisplay = `"${meta.flagAssignedValue}"` if(typeof meta.flagAssignedValue !== 'string') valDisplay = meta.flagAssignedValue; const message = `Value ${valDisplay} (castable:"${castableType}") is not assignable to required type "${flag.type}".`; const error = new InputError(message); error.context = { flag: flag.config, flagType: flag.type, flagMatch: meta.flagKey, value: meta.flagAssignedValue, assignableType: castableType, argv }; giveError(opts, { error, message: message + `\n - Flag: "${flag.config.name}", match: "${meta.flagKey}", value: ${valDisplay}, assignableType: "${castableType}", requiredType: "${flag.type}"`, exitCode: 1 }); } /** * @throws if behaviorOnInputError == 'throw' * @param {DefaultParserOptsType} opts * @param {string} flagKey * @param {any} flagVal * @param {string[]} argv - For the error context */ function unknownFlagError(opts, flagKey, flagVal, argv) { const message = `Received unknown flag "${flagKey}", but opts.allowUnknownFlags is disabled!`; const error = new InputError(message); error.context = { flagKey, flagVal, argv }; let valDisplay = `"${flagVal}"` if(typeof flagVal !== 'string') valDisplay = flagVal giveError(opts, { error, message: message + `\n - Flag: , match: "${flagKey}", value: ${valDisplay}`, exitCode: 2 }); } //#region SubworkFunctions /** * @param {DefaultParserOptsType} opts * @returns {string[]} */ function getAllShorthandFlags(opts) { const allShorthandFlags = [] Object.values(opts.flags).forEach((flag) => { allShorthandFlags.push(...flag.shorthands); if(opts.automaticBooleanFlagNegation.shorthandNegation && flag.type === 'boolean') { // For typescript ok(Array.isArray(flag.shorthands), 'Array.isArray(flag.shorthands): Should have been parsed by validateAndFillDefaults and made into an array!'); allShorthandFlags.push(...flag.shorthands.map((shorthand) => "n" + shorthand)); } }); return allShorthandFlags; } /** * Can (only if negated flag) @modify flag.key && flag.config * ## To be ran 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) { /** Cannot use flag.type as it's not yet initialized, and useless to set it as it will always be done right after this function call */ const flagType = flagConfig?.type || opts.defaultFlagType; /** @type {ReturnType} */ let castableType = 'boolean'; if(flag.value !== UNSET_FLAG_VALUE) /// @ts-expect-error - flag.value is always string at this moment in time castableType = getCastableType(flag.value); if(castableType === 'boolean' && isTypeAssignable(flagType, castableType)) { flag.key = newKey; flag.config = flagConfig; flag.isNegated = true; return true; } } return false; } /** * @related getCastableType,isTypeAssignable * @param {DefaultParserOptsType} opts * @param {string} flagVal * @param {FlagAny['type']} [expectedType] */ function runFlagValueTypeResolving(opts, flagVal, expectedType) { /** @type {any} */ let resolvedFlagVal = flagVal; if(expectedType !== undefined && expectedType === 'string') return flagVal; /** boolean checking */ if(flagVal.toLowerCase() === 'true') resolvedFlagVal = true; else if(flagVal.toLowerCase() === 'false') resolvedFlagVal = false; else if(opts.resolveFlagValueTypes) { /** bigint checking */ /// @ts-expect-error - isNaN on string if(flagVal.length >= 2 && flagVal.endsWith('n') && !isNaN(flagVal.slice(0, -1))) resolvedFlagVal = BigInt(flagVal.slice(0, -1)); /** number & bigint checking */ /// @ts-expect-error - isNaN on string else if(!isNaN(flagVal)) { if(expectedType === undefined || expectedType === 'number' || expectedType === 'any') resolvedFlagVal = Number(flagVal); else if (expectedType !== undefined && expectedType === 'bigint') resolvedFlagVal = BigInt(flagVal); } } return resolvedFlagVal; } /** * @related getCastableType,isTypeAssignable * @param {DefaultParserOptsType} opts * @param {ReturnType} meta * @param {InternalFlagsFlagObj} flag * @param {string[]} argv - For the error context */ function runFlagAssignedValueAssignment(opts, meta, flag, argv) { if(flag.config !== undefined && !isTypeAssignable(flag.type, getCastableType(meta.flagAssignedValue))) { assignedValueUncastableTypeError(opts, meta, flag, argv); return false; } const flagVal = runFlagValueTypeResolving(opts, meta.flagAssignedValue, flag.type); if(flag.isNegated && typeof flagVal === 'boolean' && isTypeAssignable(flag.type, 'boolean')) { flag.value = !flagVal } else { flag.value = flagVal; } return true; } //#endregion SubworkFunctions //#region SecondaryParsers /** * @param {string} arg * @param {DefaultParserOptsType} opts */ function parseArg(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 }; } /** * @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, config: ReturnType, isNegated: boolean, type: FlagAny['type'] }} 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, unparsed: string[] } }} */ 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); /** @throwable Will throw if anything is wrong */ validateAndFillDefaults(opts); if(opts.addFlagShorthandsAsAliases) { Object.values(opts.flags).forEach((flagConfig) => { if(flagConfig.shorthands.length >= 1) /// @ts-ignore flagConfig.aliases.push(...flagConfig.shorthands) }) } const allShorthandFlags = getAllShorthandFlags(opts); /** @type {InternalFlagsFlagObj[]} */ const flags = []; /** @type {string[]} */ const input = []; /** @type {string[]} */ const unparsed = []; /** @readonly Internal value for keeping track previous flag being available for a natural flag value assignment (ie: ['--flag', 'hi'] -> { "flag": 'hi' }) */ let naturalFlagAssignment = { isLookingForValue: false, /** @type {FlagAny['type']} */ requiredType: null }; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; const meta = parseArg(arg, opts); /** @type {argMeta} */ const argMeta = { ...meta, isShorthand: meta.minusSignCount === 1, isInput: !meta.isFlag, isFlagNaturalValue: false } if(!meta.isFlag && naturalFlagAssignment.isLookingForValue && isTypeAssignable(naturalFlagAssignment.requiredType, getCastableType(arg))) { argMeta.isFlagNaturalValue = true; argMeta.isInput = false; } //#region Parsing filters if(typeof opts.shouldStopParsingFunc === 'function') { let shouldStop = false; try { shouldStop = opts.shouldStopParsingFunc(arg, i, argv, argMeta, input); } catch (stopParsingFuncError) { const error = new EvalError(`Function opts.shouldStopParsingFunc threw an error!`); error.cause = stopParsingFuncError; /// @ts-ignore error.context = { arg, index: i, argMeta, stopParsingFunc: opts.shouldStopParsingFunc, argv }; throw error; } if(shouldStop) { unparsed.push(...argv.slice(i)); break; } } if(typeof opts.shouldStopParsingStr === 'string' && opts.shouldStopParsingStr === arg) { unparsed.push(...argv.slice(i)) break; } if(typeof opts.parseFilterFunc === 'function') { let filtered = false; try { filtered = opts.parseFilterFunc(arg, i, argv, argMeta, input); } catch (parseFilterError) { const error = new EvalError(`Function opts.parseFilterFunc threw an error!`); error.cause = parseFilterError; /// @ts-ignore error.context = { arg, index: i, argMeta, parseFilterFunc: opts.parseFilterFunc, argv }; throw error; } if(filtered) { continue; } } //#endregion Parsing filters 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(argMeta.isShorthand && !(opts.allowSingularDashLongFlags && hasFullFlag(opts, meta.flagKey))) { parseShorthandFlags(allShorthandFlags, meta.flagKey) .forEach((shorthandKey) => { /** @readonly */ let flagKey = shorthandKey; if(opts.lowerCaseFlags) flagKey = flagKey.toLowerCase(); /** @type {InternalFlagsFlagObj} */ const flag = { key: flagKey, value: UNSET_FLAG_VALUE, isShorthand: true, config: null, isNegated: false, type: opts.defaultFlagType }; runAutomaticBooleanFlagNegation(opts, flag); assureValidFlagConfigState(opts, flag); if(flag.config !== undefined) flag.type = flag.config.type; flags.push(flag); }); const flag = flags.at(-1); if(meta.hasFlagAssignedValue) { naturalFlagAssignment = { isLookingForValue: false, requiredType: null }; runFlagAssignedValueAssignment(opts, meta, flag, argv); continue; } if(flag.config === undefined || flag.config.acceptsNaturalValue) naturalFlagAssignment = { isLookingForValue: true, requiredType: flag.type }; continue; } /** @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(); /** @type {InternalFlagsFlagObj} */ const flag = { key: flagKey, value: UNSET_FLAG_VALUE, isShorthand: false, config: null, isNegated: false, type: opts.defaultFlagType }; runAutomaticBooleanFlagNegation(opts, flag); assureValidFlagConfigState(opts, flag); if(flag.config !== undefined) flag.type = flag.config.type; if(meta.hasFlagAssignedValue) { naturalFlagAssignment = { isLookingForValue: false, requiredType: null }; const succeeded = runFlagAssignedValueAssignment(opts, meta, flag, argv); if(succeeded) flags.push(flag); continue; } flags.push(flag); if(flag.config === undefined || flag.config.acceptsNaturalValue) naturalFlagAssignment = { isLookingForValue: true, requiredType: flag.type }; continue; } 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(!naturalFlagAssignment.isLookingForValue) { if(opts.lowerCaseInputValues) inputVal = inputVal.toLowerCase(); input.push(inputVal); continue } const flag = flags.at(-1); if(isTypeAssignable(flag.type, getCastableType(inputVal))) { const flagVal = runFlagValueTypeResolving(opts, inputVal, flag.type); if(flag.isNegated && typeof flagVal === 'boolean' && isTypeAssignable(flag.type, 'boolean')) { flag.value = !flagVal } else { flag.value = flagVal; } } else { if(opts.lowerCaseInputValues) inputVal = inputVal.toLowerCase(); input.push(inputVal); } naturalFlagAssignment = { isLookingForValue: false, requiredType: null }; continue; } } 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) => { if(flag.value === UNSET_FLAG_VALUE) { if(flag.config !== undefined) { if(flag.config.requiredValue === true) { const message = `Flag "${flag.config.name}" is missing a required value of type "${flag.config.type}"!`; const error = new InputError(message); error.context = { flag: flag.config, flagType: flag.config.type, flagMatch: flag.key, value: UNSET_FLAG_VALUE, argv }; return giveError(opts, { error, message: message + `\n - Flag: "${flag.config.name}", match: "${flag.key}", value: UNSET_FLAG_VALUE, type: "${flag.config.type}"`, exitCode: 1 }); } /** Set default except booleans (since this function only runs if the flag is present, and a boolean flag being present is by itself a true value (unless negated)) */ if(typeof flag.config.default !== 'undefined' && flag.config.type !== 'boolean') flag.value = flag.config.default; } if(flag.value === UNSET_FLAG_VALUE) { switch(flag.type) { case 'any': case 'boolean': /** ProCoder: flag.value = !flag.isNegated */ flag.value = true; if(flag.isNegated) flag.value = false; break; case "bigint": case "number": case "string": flag.value = null; break; } } } else { if(typeof flag.value === 'string') flag.value = runFlagValueTypeResolving(opts, flag.value, flag.type); if(typeof flag.value === 'string' && opts.lowerCaseFlagValues) flag.value = flag.value.toLowerCase() } if(flag.config === undefined && !opts.allowUnknownFlags) return unknownFlagError(opts, flag.key, flag.value, argv); const flagName = flag.config?.name || flag.key; flagReturnObj[flagName] = 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 explicitly highlight that behavior in the code */ if(typeof flagConfig.default === 'undefined') return flagReturnObj[flagName] = undefined; flagReturnObj[flagName] = flagConfig.default; }); /// @ts-ignore - falsly wrong flags type return { flags: flagReturnObj, input, unparsed }; } module.exports = { parser, InputError };