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 };