From 5533706b305c01461da17216d57f29091ae7b456 Mon Sep 17 00:00:00 2001 From: Oxtaly Date: Tue, 13 May 2025 02:12:12 +0200 Subject: [PATCH] Additions & fixes, mainly filtering options & internal refactors Additions: Filtering!: opts.shouldStopParsingFunc(): more information in commit's types.d.ts; opts.shouldStopParsingStr: more information in commit's types.d.ts; opts.parseFilterFunc(): more information in commit's types.d.ts; (tweaked (lookingForFlagVal -> naturalFlagAssignment) to include the flag's type to add onto new argMeta for filtering functions); isTypeAssignable() assureValidFlagConfigState(): Now guarantees that flag.config is assigned right after runAutomaticBooleanFlagNegation() in the flag branch of arg handling, skipping the need for other flagConfig lookups (as it would be required in one step or another regardless); runFlagAssignedValueAssignment(): extracted main code of meta.hasAssignedValue branch into this function (also adjusted to account for flag.isNegated); internalFlag.isNegated: set by runAutomaticBooleanFlagNegation(), for refactored boolean flag negation handling; internalFlag.type: extracted flagType assignments into guaranteed Removals: internalFlag.lookedupFlagConfig: flag.config is now assured with assureValidFlagConfigState() after runAutomaticBooleanFlagNegation(), and is instead tested against flag.config === null within assureValidFlagConfigState() to avoid repeating getting flag; Fixes: expectType(): Removed unused (and broken) null handling from; validateOrDefaultIfUnset(): Fixed passing through to expectType() when setting the default, breaking if the default type isn't the 'required' type (ie: null value defaults); opts.lowerCaseFlags now correctly applies to shorthand flags; flags with an explicit string type now correctly do not apply opts.resolveFlagValueTypes even if enabled; defined boolean type flags that undergo automatic boolean negation (ie: --no-flag) now correctly resolve if there is an input value after it (bug with a defined flag: `['--no-flag', 'hi'] -> { 'no-flag': 'hi' }`); Fixed some spelling issues (eg: 2x otps -> opts); Other: Copied StringConsumer from OxLib into index.js Moved stage of runAutomaticBooleanFlagNegation() from beginning of flag post arg handling -> pre flag assignments in arg handling, with a small refactor; -> Addition of internalFlag.isNegated Cleaned up and clarified the purpose of isAssignedValueTypeAssignable() -> assignedValueUncastableTypeError() to externalize the check thanks to new isTypeAssignable(); Cleaned up and clarified the purpose of unknownFlag() -> unknownFlagError() to externalize the check; Main arg handling 'loop' is not a for loop instead of a foreach to allow for a break statement for the new opts.shouldStopParsing* options; -Note: Other small unmentioned things, and big commit, probably forgot some note-worthy things, mb; --- index.js | 569 ++++++++++++++++++++++++++++++++++------------------- types.d.ts | 56 ++++++ 2 files changed, 424 insertions(+), 201 deletions(-) diff --git a/index.js b/index.js index 8a88585..2d46952 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,85 @@ -const { StringConsumer } = require('../OxLib/utils/utils') + +/** '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'); /** @@ -31,6 +111,10 @@ const { ok } = require('node:assert'); * @typedef {import('./types.d.ts').FlagAny} FlagAny */ +/** + * @typedef {import('./types.d.ts').argMeta} argMeta + */ + class InputError extends Error { context = null; }; @@ -43,23 +127,20 @@ const UNSET_FLAG_VALUE = new UnsetFlagValueType(); * @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; +function expectType(key, type, value, acceptsUndefined) { 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!`); + if(acceptsUndefined) + return else - return; -} -if(typeof value !== type) - throw new TypeError(`Expected ${key} to be ${type}, received ${typeof value}!`); + 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}!`); } /** @@ -72,10 +153,10 @@ if(typeof value !== type) */ function validateOrDefaultIfUnset(obj, keyName, keyType, defaultVal, objName) { if(typeof objName === 'undefined') - objName = 'otps'; + objName = 'opts'; if(typeof obj[keyName] === 'undefined') - obj[keyName] = defaultVal; - expectType(`${objName}.${String(keyName)}`, keyType, obj[keyName], true, false) + return obj[keyName] = defaultVal; + return expectType(`${objName}.${String(keyName)}`, keyType, obj[keyName], true) } /** @@ -87,7 +168,7 @@ function validateOrDefaultIfUnset(obj, keyName, keyType, defaultVal, objName) { * @param {any} defaultVal */ function namedValidateOrDefaultIfUnset(objName, obj, keyName, keyType, defaultVal) { - validateOrDefaultIfUnset(obj, keyName, keyType, defaultVal, objName); + return validateOrDefaultIfUnset(obj, keyName, keyType, defaultVal, objName); } /** @@ -105,16 +186,29 @@ function validateAndFillDefaults(opts) { 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}"!`); + 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(" | ")}, received "${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); @@ -128,13 +222,12 @@ function validateAndFillDefaults(opts) { 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); + expectType(`flag["${flagName}"]`, 'object', flagData, false); if(typeof flagData.type === 'undefined') { opts.warningLogger(`warn: flag["${flagName}"].type is undefined, defaulting to '${opts.defaultFlagType}'`); @@ -169,7 +262,7 @@ function validateAndFillDefaults(opts) { 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 + //#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 @@ -300,12 +393,25 @@ 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 + * @related runFlagValueTypeResolving,isTypeAssignable * @param {string} input * @returns {"number"|"bigint"|"boolean"|"string"} */ @@ -321,6 +427,21 @@ function getCastableType(input) { 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 @@ -337,62 +458,46 @@ function giveError(opts, errorObj) { } /** - * @throws if otps.behaviorOnInputError == 'throw' and flag value isn't assignable to it's required type + * @throws if opts.behaviorOnInputError == 'throw' && flag value isn't assignable to it's required type * @param {DefaultParserOptsType} opts - * @param {ReturnType} meta + * @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; - } - +function assignedValueUncastableTypeError(opts, meta, flag, argv) { 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; + 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' and opts.allowUnknownFlags == false and flag isn't found in the flag config + * @throws if behaviorOnInputError == 'throw' * @param {DefaultParserOptsType} opts * @param {string} flagKey * @param {any} flagVal - * @param {string[]} argv - For the errror context + * @param {string[]} argv - For the error 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; +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 @@ -405,9 +510,9 @@ 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 + // 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)); } }); @@ -415,8 +520,8 @@ function getAllShorthandFlags(opts) { } /** - * @modifies flag - * ### To be run before any flag value assignments (flag.value always expected to be string) + * 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 */ @@ -436,7 +541,6 @@ function runAutomaticBooleanFlagNegation(opts, flag) { 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; @@ -458,31 +562,21 @@ function runAutomaticBooleanFlagNegation(opts, flag) { } } } + if(negated) { - /** @readonly */ - let flagType = opts.defaultFlagType; - if(typeof flagConfig !== 'undefined') - flagType = flagConfig.type; + /** 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-ignore + /// @ts-expect-error - flag.value is always string at this moment in time castableType = getCastableType(flag.value); - if((flagType === 'boolean' || flagType === 'any') && castableType === 'boolean') { + if(castableType === 'boolean' && isTypeAssignable(flagType, castableType)) { 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; - } + flag.config = flagConfig; + flag.isNegated = true; return true; } } @@ -490,16 +584,18 @@ function runAutomaticBooleanFlagNegation(opts, flag) { } /** - * @related getCastableType - * ### Warning: This does not check the flag type despite passing in a flagConfig! + * @related getCastableType,isTypeAssignable * @param {DefaultParserOptsType} opts * @param {string} flagVal - * @param {ReturnType} flagConfig + * @param {FlagAny['type']} [expectedType] */ -function runFlagValueTypeResolving(opts, flagVal, flagConfig) { +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; @@ -507,29 +603,52 @@ function runFlagValueTypeResolving(opts, flagVal, flagConfig) { resolvedFlagVal = false; else if(opts.resolveFlagValueTypes) { /** bigint checking */ - /// @ts-ignore + /// @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-ignore + /// @ts-expect-error - isNaN on string else if(!isNaN(flagVal)) { - if(typeof flagConfig === 'undefined' || flagConfig.type === 'number' || flagConfig.type === 'any') + if(expectedType === undefined || expectedType === 'number' || expectedType === 'any') resolvedFlagVal = Number(flagVal); - else if (typeof flagConfig !== 'undefined' && flagConfig.type === 'bigint') + 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 + * @param {DefaultParserOptsType} opts */ -function argMetaCreator(arg, opts) { +function parseArg(arg, opts) { const argsStr = new StringConsumer(arg); /** return meta flags */ @@ -624,8 +743,6 @@ function argMetaCreator(arg, opts) { 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 @@ -676,7 +793,7 @@ function parseShorthandFlags(shorthandFlags, shorthandString) { /** * @interface - * @typedef {{ key: string, value: typeof UNSET_FLAG_VALUE | number | boolean | string, isShorthand: boolean, lookedupFlagConfig: boolean, flagConfig: ReturnType }} InternalFlagsFlagObj + * @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 @@ -691,13 +808,13 @@ function parseShorthandFlags(shorthandFlags, shorthandString) { * @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 } }} + * @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, false); + expectType('opts', 'object', opts, false); /** @throwable Will throw if anything is wrong */ validateAndFillDefaults(opts); @@ -715,53 +832,128 @@ function parser(argv, opts) { const flags = []; /** @type {string[]} */ const input = []; + /** @type {string[]} */ + const unparsed = []; - let lookingForFlagVal = false; - argv.forEach((arg) => { - const meta = argMetaCreator(arg, opts); + /** @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(meta.minusSignCount === 1 && (!opts.allowSingularDashLongFlags || !hasFullFlag(opts, meta.flagKey))) { - if(opts.lowerCaseFlags) - arg = arg.toLowerCase(); - + if(argMeta.isShorthand && !(opts.allowSingularDashLongFlags && hasFullFlag(opts, meta.flagKey))) { parseShorthandFlags(allShorthandFlags, meta.flagKey) - .forEach((arg) => flags.push({ key: arg, value: UNSET_FLAG_VALUE, isShorthand: true, lookedupFlagConfig: false, flagConfig: null })); - + .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) { - const flag = flags.at(-1); - /** @throwable Can throw depending on opts.behaviorOnInputError */ - if(isAssignedValueTypeAssignable(opts, meta, flag, argv)) - flag.value = meta.flagAssignedValue; + naturalFlagAssignment = { isLookingForValue: false, requiredType: null }; + runFlagAssignedValueAssignment(opts, meta, flag, argv); + continue; } - else { - lookingForFlagVal = true; - } - return; + + 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(); - - const flagConfig = getFullFlag(opts, flagKey); + + /** @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) { - /** @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; + naturalFlagAssignment = { isLookingForValue: false, requiredType: null }; + const succeeded = runFlagAssignedValueAssignment(opts, meta, flag, argv); + if(succeeded) + flags.push(flag); + continue; } - flags.push({ key: flagKey, value: UNSET_FLAG_VALUE, isShorthand: false, lookedupFlagConfig: true, flagConfig }); - lookingForFlagVal = true; - return; + flags.push(flag); + naturalFlagAssignment = { isLookingForValue: true, requiredType: flag.type }; + continue; } else { @@ -771,84 +963,61 @@ function parser(argv, opts) { if(opts.handleBackslashesForSpecialFlagChars && inputVal.startsWith('\\-')) inputVal = inputVal.slice(1); - if(!lookingForFlagVal) { + if(!naturalFlagAssignment.isLookingForValue) { if(opts.lowerCaseInputValues) inputVal = inputVal.toLowerCase(); input.push(inputVal); - return + continue } 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; + + 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; + } } - - - 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; + 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) => { - /** 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}"!`; + 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: flagConfig, flagType: flagConfig.type, flagMatch: flag.key, value: UNSET_FLAG_VALUE, argv }; + 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: "${flagConfig.name}", match: "${flag.key}", value: UNSET_FLAG_VALUE, type: "${flagConfig.type}"`, + message: message + `\n - Flag: "${flag.config.name}", match: "${flag.key}", value: UNSET_FLAG_VALUE, type: "${flag.config.type}"`, exitCode: 1 }); } - /** Set default unless bool type */ - if(typeof flagConfig.default !== 'undefined' && flagConfig.type !== 'boolean') - flag.value = flagConfig.default; + /** 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(flagType) { + 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": @@ -857,35 +1026,33 @@ function parser(argv, opts) { 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(typeof flag.value === 'string') - flag.value = runFlagValueTypeResolving(opts, flag.value, flagConfig); - 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; - let realFlagKey = flag.key; - if(flagConfig !== undefined) { - realFlagKey = flagConfig.name; - } - else - unknownFlag(opts, flag.key, flag.value, argv); - - flagReturnObj[realFlagKey] = flag.value; + 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 explicitely highlight that behavior in the code */ + /** 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 - return { flags: flagReturnObj, input }; + /// @ts-ignore - falsly wrong flags type + return { flags: flagReturnObj, input, unparsed }; } -module.exports = { parser }; \ No newline at end of file +module.exports = { parser, InputError }; \ No newline at end of file diff --git a/types.d.ts b/types.d.ts index a93c839..5461ca2 100644 --- a/types.d.ts +++ b/types.d.ts @@ -84,6 +84,62 @@ export interface ParserOpts { handleBackslashesForSpecialFlagChars?: boolean /** If enabled, automatically adds shorthands as valid flag aliases (eg: -f &-> --f) @default true */ addFlagShorthandsAsAliases?: boolean + /** + * If a function is provided, stops parsing when that function returns a truthy value. + * + * Adds any following args (including the matching arg) into the returned unparsed array. + * + * **Overrides shouldStopParsingStr** + * @param arg - to-be parsed arg + * @param index - index of the arg within the argv array + * @param argv - argv array + * @param argMeta - meta object containing informations about the arg (*properties subject to change) + * @param input - input array, containing the currently parsed input values + * @default null + */ + shouldStopParsingFunc?(arg: string, index: number, argv: string[], argMeta: argMeta, input: string[]): boolean + /** + * If a string is provided, stops parsing if the to-be parsed arg matches the string provided exactly. + * The to-be parsed arg is not yet lowercased by any lowerCase* options, it is therefore recommended to use shouldStopParsingFunc instead if that is a requirement. + * + * Adds any following args (including the matching arg) into the returned unparsed array. + * + * **Overridden by shouldStopParsingFunc** + * @default null + */ + shouldStopParsingStr?: null | string + /** + * If a function is provided, will only parse the provided arg if the provided function returns a truthy value. + * + * Runs after shouldStopParsingFunc + * + * Does not add filtered out args into the returned unparsed array. + * + * @param arg - to-be parsed arg + * @param index - index of the arg within the argv array + * @param argv - argv array + * @param argMeta - meta object containing informations about the arg (*properties subject to change) + * @param input - input array, containing the currently parsed input values + * @default null + */ + parseFilterFunc?(arg: string, index: number, argv: string[], argMeta: argMeta, input: string[]): boolean +} + +export type argMeta = { + /** Is set to true if the arg will be parsed as a flag */ + isFlag: boolean; + /** Is isFlag is true, the flag will be parsed as a shorthand */ + isShorthand: boolean; + /** Is isFlag is true, is set to the flag key without any leading flag characters (eg: `"--flag" -> "flag"`) */ + flagKey: string; + /** Is true if isFlag is true and the flag has an assigned value (ie: `arg === "--flag=value"`) */ + hasFlagAssignedValue: boolean; + /** Is hasFlagAssignedValue is true, is set to the flag's assigned value (eg: `"--flag=value" -> "value"`) */ + flagAssignedValue: string; + /** Is set to true if the arg will be treated as an input string, ie: if isFlag and isFlagNaturalValue are false */ + isInput: boolean; + /** Is set to true if this arg would be treated as the value to the most recently parsed flag (ie: `["--flag", "value"][1].argMeta.isFlagNaturalValue === true` ) */ + isFlagNaturalValue: boolean; } function __type__getType(e: any) { return typeof e };