commit aef5a62455752bf1d4918d4f3cf297e5fac7ede0 Author: Oxtaly Date: Fri May 9 15:55:57 2025 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f83526d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/node_modules/ \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..7c2fef3 --- /dev/null +++ b/index.js @@ -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} 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 + */ + +/** + * @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` + //#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"|"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} 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: , 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 }} 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); + 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 }; \ No newline at end of file diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..1907dfb --- /dev/null +++ b/jsconfig.json @@ -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/*" + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a26180e --- /dev/null +++ b/package-lock.json @@ -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" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7a0cefe --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..9a55891 --- /dev/null +++ b/types.d.ts @@ -0,0 +1,66 @@ + +interface FlagT { + 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 | FlagT | FlagT | FlagT + +export interface FlagsI { [key: readonly string]: FlagAny } + +export type DefaultParserOptsType = ParserOpts + +/** This object will be modified by parser */ +export interface ParserOpts { + 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 \ No newline at end of file