1080 lines
40 KiB
JavaScript
1080 lines
40 KiB
JavaScript
// ox-argv-wrapper
|
|
// Copyright (C) 2025 Oxtaly
|
|
|
|
// // This program is free software: you can redistribute it and/or modify
|
|
// // it under the terms of the GNU General Public License as published by
|
|
// // the Free Software Foundation, either version 3 of the License, or
|
|
// // (at your option) any later version.
|
|
|
|
// // This program is distributed in the hope that it will be useful,
|
|
// // but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// // GNU General Public License for more details.
|
|
|
|
// // You should have received a copy of the GNU General Public License
|
|
// // along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
/** 'bundling' my own dependencies */
|
|
class StringConsumer {
|
|
/** @readonly */
|
|
static VERSION = 3;
|
|
|
|
/**
|
|
* @param {string} str
|
|
*/
|
|
constructor(str) {
|
|
/** @private */
|
|
this.base = str;
|
|
/** @private */
|
|
this.i = 0;
|
|
}
|
|
|
|
/**
|
|
* Returns the index of the next character in line
|
|
* If getPointer() == getFullStr().length, hasNext() = false
|
|
*/
|
|
getPointer() {
|
|
return this.i;
|
|
}
|
|
|
|
getFullStr() {
|
|
return this.base;
|
|
}
|
|
|
|
/**
|
|
* Get the next characters while consuming them
|
|
* @param {number} [num]
|
|
*/
|
|
next(num) {
|
|
if(num < 1)
|
|
throw new RangeError(`Num ${num} < 1! Num must be between 1 and fullStr length.`)
|
|
if(typeof num !== 'number')
|
|
num = 1;
|
|
if(this.i == this.base.length)
|
|
return null;
|
|
let retStr = this.base.slice(this.i, this.i + num);
|
|
this.i += num;
|
|
if(this.i >= this.base.length)
|
|
this.i = this.base.length;
|
|
return retStr;
|
|
}
|
|
|
|
/**
|
|
* Get the next characters without consuming them
|
|
* @param {number} [num]
|
|
*/
|
|
peek(num) {
|
|
if(num < 1)
|
|
throw new RangeError(`Num ${num} < 1! Num must be between 1 and fullStr length.`)
|
|
if(typeof num !== 'number')
|
|
num = 1;
|
|
if(this.i == this.base.length)
|
|
return null;
|
|
let retStr = this.base.slice(this.i, this.i + num);
|
|
return retStr;
|
|
}
|
|
|
|
reset() {
|
|
this.i = 0;
|
|
}
|
|
|
|
end() {
|
|
return this.next(this.base.length-this.i);
|
|
}
|
|
|
|
/**
|
|
* @param {number} [num]
|
|
*/
|
|
hasNext(num) {
|
|
if(num < 1)
|
|
throw new RangeError(`Num ${num} < 1! Num must be between 1 and fullStr length.`)
|
|
if(typeof num !== 'number')
|
|
num = 1;
|
|
return this.i + num <= this.base.length;
|
|
}
|
|
}
|
|
|
|
const { ok } = require('node:assert');
|
|
|
|
/**
|
|
* @interface
|
|
* @template {FlagsI} Flags
|
|
* @typedef {import('./types.d.ts').ParserOpts<Flags>} ParserOpts
|
|
*/
|
|
|
|
/**
|
|
* @typedef {import('./types.d.ts').DefaultParserOptsType} DefaultParserOptsType
|
|
*/
|
|
|
|
/**
|
|
* @template T
|
|
* @template Z
|
|
* @typedef {import('./types.d.ts').FlagT<T, Z>} FlagT
|
|
*/
|
|
|
|
/**
|
|
* @interface
|
|
* @typedef {import('./types.d.ts').FlagsI} FlagsI
|
|
*/
|
|
|
|
/**
|
|
* @typedef {import('./types.d.ts').JSTypes} JSTypes
|
|
*/
|
|
|
|
/**
|
|
* @typedef {import('./types.d.ts').FlagAny} FlagAny
|
|
*/
|
|
|
|
/**
|
|
* @typedef {import('./types.d.ts').argMeta} argMeta
|
|
*/
|
|
|
|
class InputError extends Error {
|
|
context = null;
|
|
};
|
|
|
|
class UnsetFlagValueType {}
|
|
const UNSET_FLAG_VALUE = new UnsetFlagValueType();
|
|
|
|
/**
|
|
* @param {string} key
|
|
* @param {JSTypes} type
|
|
* @param {any} value
|
|
* @param {boolean} [acceptsUndefined=false]
|
|
*/
|
|
function expectType(key, type, value, acceptsUndefined) {
|
|
if(typeof acceptsUndefined !== 'boolean')
|
|
acceptsUndefined = false;
|
|
|
|
if(typeof value === 'undefined') {
|
|
if(acceptsUndefined)
|
|
return
|
|
else
|
|
throw new TypeError(`Required type ${type} for ${key} is undefined!`);
|
|
}
|
|
|
|
if(typeof value !== type)
|
|
throw new TypeError(`Expected ${key} to be ${type}, received ${typeof value}!`);
|
|
}
|
|
|
|
/**
|
|
* @template {Object} obj
|
|
* @param {obj} obj
|
|
* @param {keyof obj} keyName
|
|
* @param {JSTypes} keyType
|
|
* @param {any} defaultVal
|
|
* @param {string} [objName='opts']
|
|
*/
|
|
function validateOrDefaultIfUnset(obj, keyName, keyType, defaultVal, objName) {
|
|
if(typeof objName === 'undefined')
|
|
objName = 'opts';
|
|
if(typeof obj[keyName] === 'undefined')
|
|
return obj[keyName] = defaultVal;
|
|
return expectType(`${objName}.${String(keyName)}`, keyType, obj[keyName], true)
|
|
}
|
|
|
|
/**
|
|
* @template {Object} obj
|
|
* @param {string} objName
|
|
* @param {obj} obj
|
|
* @param {keyof obj} keyName
|
|
* @param {JSTypes} keyType
|
|
* @param {any} defaultVal
|
|
*/
|
|
function namedValidateOrDefaultIfUnset(objName, obj, keyName, keyType, defaultVal) {
|
|
return validateOrDefaultIfUnset(obj, keyName, keyType, defaultVal, objName);
|
|
}
|
|
|
|
/**
|
|
* @authorsComments: Not my prettiest work but it works so hey
|
|
* @throws
|
|
* @param {DefaultParserOptsType} opts
|
|
*/
|
|
function validateAndFillDefaults(opts) {
|
|
|
|
const allowedTypeofDefaultType = ["string", "number", "bigint", "boolean"];
|
|
const validFlagTypes = ['any', ...allowedTypeofDefaultType];
|
|
|
|
const allowedBehaviorOnInputErrorTypes = ['throw', 'log', 'log&exit', 'ignore'];
|
|
|
|
validateOrDefaultIfUnset(opts, 'warningLogger', 'function', console.log);
|
|
validateOrDefaultIfUnset(opts, 'behaviorOnInputError', 'string', 'throw');
|
|
if(!allowedBehaviorOnInputErrorTypes.includes(opts.behaviorOnInputError))
|
|
throw new TypeError(`Expected opts.behaviorOnInputError === ${allowedBehaviorOnInputErrorTypes.map((type) => `"${type}"`).join(" | ")} | undefined, received "${opts.behaviorOnInputError}"!`);
|
|
validateOrDefaultIfUnset(opts, 'defaultFlagType', 'string', 'any');
|
|
if(!validFlagTypes.includes(opts.defaultFlagType))
|
|
throw new TypeError(`Expected opts.defaultFlagType === ${validFlagTypes.map((type) => `"${type}"`).join(" | ")} | undefined, received "${opts.defaultFlagType}"!`);
|
|
|
|
/** Category: @default false */
|
|
validateOrDefaultIfUnset(opts, 'allowSingularDashLongFlags', 'boolean', false);
|
|
validateOrDefaultIfUnset(opts, 'lowerCaseFlagValues', 'boolean', false);
|
|
validateOrDefaultIfUnset(opts, 'lowerCaseInputValues', 'boolean', false);
|
|
|
|
if(opts.shouldStopParsingFunc !== null)
|
|
validateOrDefaultIfUnset(opts, 'shouldStopParsingFunc', 'function', null);
|
|
if(opts.shouldStopParsingStr !== null)
|
|
validateOrDefaultIfUnset(opts, 'shouldStopParsingStr', 'string', null);
|
|
|
|
if(opts.shouldStopParsingStr !== null && opts.shouldStopParsingFunc !== null) {
|
|
opts.warningLogger(`warn: opts.shouldStopParsingStr and opts.shouldStopParsingFunc are both present, opts.shouldStopParsingStr will be ignored!`);
|
|
opts.shouldStopParsingStr = null;
|
|
}
|
|
|
|
if(opts.parseFilterFunc !== null)
|
|
validateOrDefaultIfUnset(opts, 'parseFilterFunc', 'function', null);
|
|
|
|
/** Category: @default true */
|
|
validateOrDefaultIfUnset(opts, 'allowUnknownFlags', 'boolean', true);
|
|
validateOrDefaultIfUnset(opts, 'lowerCaseFlags', 'boolean', true);
|
|
validateOrDefaultIfUnset(opts, 'resolveFlagValueTypes', 'boolean', true);
|
|
validateOrDefaultIfUnset(opts, 'allowNullDefaultFlagValues', 'boolean', true);
|
|
validateOrDefaultIfUnset(opts, 'handleBackslashesForSpecialFlagChars', 'boolean', true);
|
|
validateOrDefaultIfUnset(opts, 'addFlagShorthandsAsAliases', 'boolean', true);
|
|
|
|
|
|
validateOrDefaultIfUnset(opts, 'automaticBooleanFlagNegation', 'object', {});
|
|
namedValidateOrDefaultIfUnset('opts.automaticBooleanFlagNegation', opts.automaticBooleanFlagNegation, 'allowUnspacedNegatedFlags', 'boolean', true);
|
|
namedValidateOrDefaultIfUnset('opts.automaticBooleanFlagNegation', opts.automaticBooleanFlagNegation, 'enabled', 'boolean', true);
|
|
namedValidateOrDefaultIfUnset('opts.automaticBooleanFlagNegation', opts.automaticBooleanFlagNegation, 'shorthandNegation', 'boolean', true);
|
|
|
|
validateOrDefaultIfUnset(opts, 'flags', "object", {});
|
|
|
|
Object.entries(opts.flags).forEach(/** @param {[string, FlagAny]} value */ ([flagName, flagData]) => {
|
|
// namedValidateOrDefaultIfUnset('flag', flagData, 'default')
|
|
expectType(`flag["${flagName}"]`, 'object', flagData, false);
|
|
|
|
if(typeof flagData.type === 'undefined') {
|
|
opts.warningLogger(`warn: flag["${flagName}"].type is undefined, defaulting to '${opts.defaultFlagType}'`);
|
|
flagData.type = opts.defaultFlagType;
|
|
}
|
|
|
|
if(!validFlagTypes.includes(flagData.type)) {
|
|
throw new TypeError(`Expected flag["${flagName}"].type === ${validFlagTypes.map((type) => `"${type}"`).join(" | ")}, received "${flagData.type}"!`);
|
|
}
|
|
|
|
/** Type of flagData.default should be consistent with flagData.type */
|
|
if(flagData.default !== undefined) {
|
|
if(flagData.default !== null && opts.allowNullDefaultFlagValues) {
|
|
if(flagData.type !== 'any' && typeof flagData.default !== flagData.type)
|
|
throw new TypeError(`Expected typeof flag["${flagName}"].default === flag["${flagName}"].type, received "${typeof flagData.default}"!`);
|
|
/** @type {JSTypes[]} */
|
|
if(flagData.type === 'any' && !allowedTypeofDefaultType.includes(typeof flagData.default))
|
|
throw new TypeError(`Expected typeof flag["${flagName}"].default === ${allowedTypeofDefaultType.map((type) => `"${type}"`).join(" | ")}, received "${typeof flagData.default}"!`);
|
|
}
|
|
}
|
|
|
|
|
|
if(typeof flagData.shorthands === 'string')
|
|
flagData.shorthands = [flagData.shorthands];
|
|
if(typeof flagData.shorthands === 'undefined')
|
|
flagData.shorthands = [];
|
|
if(!Array.isArray(flagData.shorthands) || flagData.shorthands.some((shorthand) => typeof shorthand !== 'string'))
|
|
throw new TypeError(`flag["${flagName}"].shorthands must be type string | string[] | undefined!`);
|
|
|
|
if(typeof flagData.aliases === 'undefined')
|
|
flagData.aliases = [];
|
|
if(typeof flagData.aliases !== 'undefined' && (!Array.isArray(flagData.aliases) || flagData.aliases.some((alias) => typeof alias !== 'string')))
|
|
throw new TypeError(`flag["${flagName}"].aliases must be type string[]!`);
|
|
|
|
//#region I hate that, setting useless properties that echo a value that I should supposedly already have, but it would require a non trivial refactoring in a lot of places so this is easier
|
|
//its also wrongly typed because obviously flag.name doesn't actually exist outside of me parsing the options, so it 's type is actually in `ReturnType<typeof getFlag()>`
|
|
//#endregion
|
|
///@ts-expect-error
|
|
if(typeof flagData.name !== 'undefined' && flagData.name !== flagName) {
|
|
throw new TypeError(`internal flag["${flagName}"].name must be set to flagName:"${flagName}" or undefined!`);
|
|
}
|
|
///@ts-expect-error
|
|
flagData.name = flagName;
|
|
|
|
|
|
if(typeof flagData.requiredValue === 'undefined')
|
|
flagData.requiredValue = false;
|
|
if(typeof flagData.requiredValue !== 'boolean')
|
|
throw new TypeError(`flag["${flagName}"].requiredValue must be type boolean | undefined!`);
|
|
|
|
if(typeof flagData.acceptsNaturalValue === 'undefined')
|
|
flagData.acceptsNaturalValue = true;
|
|
if(typeof flagData.acceptsNaturalValue !== 'boolean')
|
|
throw new TypeError(`flag["${flagName}"].acceptsNaturalValue must be type boolean | undefined!`);
|
|
|
|
})
|
|
}
|
|
|
|
//#region findFlagFunctions
|
|
|
|
/**
|
|
* @param {DefaultParserOptsType} opts
|
|
* @param {string} flagKeyInput
|
|
* @param {boolean} [isShorthand=false]
|
|
*/
|
|
function hasFlag(opts, flagKeyInput, isShorthand) {
|
|
if(typeof isShorthand !== 'boolean')
|
|
isShorthand = false;
|
|
|
|
/** @type {string} @readonly */
|
|
/// @ts-ignore
|
|
let flagKey = flagKeyInput
|
|
|
|
const definedFlagKeys = [];
|
|
Object.entries(opts.flags).forEach(([flagKey, flag]) => {
|
|
if(isShorthand) {
|
|
if(flag.shorthands.length >= 1)
|
|
definedFlagKeys.push(...flag.shorthands);
|
|
} else {
|
|
definedFlagKeys.push(flagKey);
|
|
if(flag.aliases.length >= 1)
|
|
definedFlagKeys.push(...flag.aliases);
|
|
}
|
|
});
|
|
|
|
if(opts.lowerCaseFlags) {
|
|
flagKey = flagKey.toLowerCase();
|
|
definedFlagKeys.forEach((flagKey) => flagKey = flagKey.toLowerCase());
|
|
}
|
|
|
|
if(definedFlagKeys.includes(flagKey))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param {DefaultParserOptsType} opts
|
|
* @param {string} flagKeyInput
|
|
* @returns {boolean}
|
|
*/
|
|
function hasShorthandFlag(opts, flagKeyInput) {
|
|
return hasFlag(opts, flagKeyInput, true);
|
|
}
|
|
|
|
/**
|
|
* @param {DefaultParserOptsType} opts
|
|
* @param {string} flagKeyInput
|
|
* @returns {boolean}
|
|
*/
|
|
function hasFullFlag(opts, flagKeyInput) {
|
|
return hasFlag(opts, flagKeyInput, false);
|
|
}
|
|
|
|
/**
|
|
* @param {DefaultParserOptsType} opts
|
|
* @param {string} flagKeyInput
|
|
* @param {boolean} [isShorthand=false]
|
|
* @returns {undefined | (FlagAny & { name: string })}
|
|
*/
|
|
function getFlag(opts, flagKeyInput, isShorthand) {
|
|
if(typeof isShorthand !== 'boolean')
|
|
isShorthand = false;
|
|
|
|
/** @type {string} @readonly */
|
|
let flagKey = flagKeyInput
|
|
|
|
/** @type {{ flagKey: string, ref: FlagAny }[]} */
|
|
const definedFlagKeys = [];
|
|
/** For correct priorities in cases of similar aliases or shorthands, ordering by definition order (or at least trying) */
|
|
Object.entries(opts.flags).forEach(([flagKey, flag]) => {
|
|
if(isShorthand) {
|
|
///@ts-expect-error
|
|
flag.shorthands.forEach((shorthand) => definedFlagKeys.push({ flagKey: shorthand, ref: flag }));
|
|
} else {
|
|
definedFlagKeys.push({ flagKey, ref: flag });
|
|
flag.aliases.forEach((alias) => definedFlagKeys.push({ flagKey: alias, ref: flag }));
|
|
}
|
|
});
|
|
|
|
if(opts.lowerCaseFlags) {
|
|
flagKey = flagKey.toLowerCase();
|
|
definedFlagKeys.forEach((definedFlagKeyRef) => definedFlagKeyRef.flagKey = definedFlagKeyRef.flagKey.toLowerCase());
|
|
}
|
|
|
|
const foundFlag = definedFlagKeys.find((definedFlagKeyRef) => definedFlagKeyRef.flagKey === flagKey);
|
|
|
|
if(foundFlag !== undefined)
|
|
/// @ts-ignore The type is correct
|
|
return foundFlag.ref;
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* @param {DefaultParserOptsType} opts
|
|
* @param {string} flagKeyInput
|
|
* @returns {ReturnType<typeof getFlag>}
|
|
*/
|
|
function getFullFlag(opts, flagKeyInput) {
|
|
return getFlag(opts, flagKeyInput, false);
|
|
}
|
|
|
|
/**
|
|
* @param {DefaultParserOptsType} opts
|
|
* @param {string} flagKeyInput
|
|
* @returns {ReturnType<typeof getFlag>}
|
|
*/
|
|
function getShorthandFlag(opts, flagKeyInput) {
|
|
return getFlag(opts, flagKeyInput, true);
|
|
}
|
|
|
|
/**
|
|
* @param {DefaultParserOptsType} opts
|
|
* @param {InternalFlagsFlagObj} flag
|
|
*/
|
|
function assureValidFlagConfigState(opts, flag) {
|
|
if(flag.config === null) {
|
|
if(flag.isShorthand)
|
|
flag.config = getShorthandFlag(opts, flag.key);
|
|
else
|
|
flag.config = getFullFlag(opts, flag.key);
|
|
}
|
|
}
|
|
|
|
//#endregion findFlagFunctions
|
|
//#region end findFlagFunctions
|
|
//#endregion end findFlagFunctions
|
|
|
|
/**
|
|
* @related runFlagValueTypeResolving,isTypeAssignable
|
|
* @param {string} input
|
|
* @returns {"number"|"bigint"|"boolean"|"string"}
|
|
*/
|
|
function getCastableType(input) {
|
|
if(input.toLowerCase() === 'true' || input.toLowerCase() === 'false')
|
|
return 'boolean';
|
|
/// @ts-ignore
|
|
if(input.length >= 2 && input.endsWith('n') && !isNaN(input.slice(0, -1)))
|
|
return 'bigint';
|
|
/// @ts-ignore
|
|
if(!isNaN(input))
|
|
return 'number';
|
|
return 'string';
|
|
}
|
|
|
|
/**
|
|
* @related runFlagValueTypeResolving,getCastableType
|
|
* @param {FlagAny['type']} baseType
|
|
* @param {FlagAny['type']} comparedType
|
|
*/
|
|
function isTypeAssignable(baseType, comparedType) {
|
|
if(baseType === comparedType)
|
|
return true;
|
|
if(baseType === 'any' || baseType === 'string')
|
|
return true;
|
|
if(baseType === 'bigint' && comparedType === 'number')
|
|
return true;
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* @param {DefaultParserOptsType} opts
|
|
* @param {{ error: InputError | Error, message: string, exitCode: number }} errorObj
|
|
*/
|
|
function giveError(opts, errorObj) {
|
|
if(opts.behaviorOnInputError === 'throw') {
|
|
throw errorObj.error;
|
|
} else if(opts.behaviorOnInputError === 'log' || opts.behaviorOnInputError === 'log&exit') {
|
|
opts.warningLogger(errorObj.message);
|
|
}
|
|
if(opts.behaviorOnInputError === 'log&exit') {
|
|
process.exit(errorObj.exitCode || 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @throws if opts.behaviorOnInputError == 'throw' && flag value isn't assignable to it's required type
|
|
* @param {DefaultParserOptsType} opts
|
|
* @param {ReturnType<typeof parseArg>} meta
|
|
* @param {InternalFlagsFlagObj} flag
|
|
* @param {string[]} argv - For the error context
|
|
*/
|
|
function assignedValueUncastableTypeError(opts, meta, flag, argv) {
|
|
const castableType = getCastableType(meta.flagAssignedValue);
|
|
let valDisplay = `"${meta.flagAssignedValue}"`
|
|
if(typeof meta.flagAssignedValue !== 'string')
|
|
valDisplay = meta.flagAssignedValue;
|
|
const message = `Value ${valDisplay} (castable:"${castableType}") is not assignable to required type "${flag.type}".`;
|
|
const error = new InputError(message);
|
|
error.context = { flag: flag.config, flagType: flag.type, flagMatch: meta.flagKey, value: meta.flagAssignedValue, assignableType: castableType, argv };
|
|
giveError(opts, {
|
|
error,
|
|
message: message + `\n - Flag: "${flag.config.name}", match: "${meta.flagKey}", value: ${valDisplay}, assignableType: "${castableType}", requiredType: "${flag.type}"`,
|
|
exitCode: 1
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @throws if behaviorOnInputError == 'throw'
|
|
* @param {DefaultParserOptsType} opts
|
|
* @param {string} flagKey
|
|
* @param {any} flagVal
|
|
* @param {string[]} argv - For the error context
|
|
*/
|
|
function unknownFlagError(opts, flagKey, flagVal, argv) {
|
|
const message = `Received unknown flag "${flagKey}", but opts.allowUnknownFlags is disabled!`;
|
|
const error = new InputError(message);
|
|
error.context = { flagKey, flagVal, argv };
|
|
let valDisplay = `"${flagVal}"`
|
|
if(typeof flagVal !== 'string')
|
|
valDisplay = flagVal
|
|
giveError(opts, {
|
|
error,
|
|
message: message + `\n - Flag: <not found>, match: "${flagKey}", value: ${valDisplay}`,
|
|
exitCode: 2
|
|
});
|
|
}
|
|
|
|
//#region SubworkFunctions
|
|
|
|
/**
|
|
* @param {DefaultParserOptsType} opts
|
|
* @returns {string[]}
|
|
*/
|
|
function getAllShorthandFlags(opts) {
|
|
const allShorthandFlags = []
|
|
Object.values(opts.flags).forEach((flag) => {
|
|
allShorthandFlags.push(...flag.shorthands);
|
|
if(opts.automaticBooleanFlagNegation.shorthandNegation && flag.type === 'boolean') {
|
|
// For typescript
|
|
ok(Array.isArray(flag.shorthands), 'Array.isArray(flag.shorthands): Should have been parsed by validateAndFillDefaults and made into an array!');
|
|
allShorthandFlags.push(...flag.shorthands.map((shorthand) => "n" + shorthand));
|
|
}
|
|
});
|
|
return allShorthandFlags;
|
|
}
|
|
|
|
/**
|
|
* Can (only if negated flag) @modify flag.key && flag.config
|
|
* ## To be ran before any flag value assignments: flag.value always expected to be string
|
|
* @param {DefaultParserOptsType} opts
|
|
* @param {InternalFlagsFlagObj} flag
|
|
*/
|
|
function runAutomaticBooleanFlagNegation(opts, flag) {
|
|
if(opts.automaticBooleanFlagNegation.enabled !== true)
|
|
return false;
|
|
if(flag.isShorthand && opts.automaticBooleanFlagNegation.shorthandNegation !== true)
|
|
return false;
|
|
|
|
/** @readonly */
|
|
let newKey = null;
|
|
/** @readonly */
|
|
let flagConfig = null;
|
|
let negated = false;
|
|
|
|
|
|
if(!flag.isShorthand) {
|
|
/** Make sure flag isn't set in our vals */
|
|
if(!hasFullFlag(opts, flag.key) && flag.key.length > 2) {
|
|
if(flag.key.length > 3 && flag.key.toLowerCase().startsWith('no-')) {
|
|
newKey = flag.key.slice(3);
|
|
negated = true;
|
|
flagConfig = getFullFlag(opts, newKey);
|
|
}
|
|
else if(opts.automaticBooleanFlagNegation.allowUnspacedNegatedFlags && flag.key.toLowerCase().startsWith('no')) {
|
|
newKey = flag.key.slice(2);
|
|
negated = true;
|
|
flagConfig = getFullFlag(opts, newKey);
|
|
}
|
|
}
|
|
} else {
|
|
/** Make sure flag isn't set in our vals */
|
|
if(!hasShorthandFlag(opts, flag.key) && flag.key.length >= 2) {
|
|
if(flag.key.toLowerCase().startsWith('n')) {
|
|
newKey = flag.key.slice(1);
|
|
negated = true;
|
|
flagConfig = getShorthandFlag(opts, newKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(negated) {
|
|
/** Cannot use flag.type as it's not yet initialized, and useless to set it as it will always be done right after this function call */
|
|
const flagType = flagConfig?.type || opts.defaultFlagType;
|
|
|
|
/** @type {ReturnType<typeof getCastableType>} */
|
|
let castableType = 'boolean';
|
|
if(flag.value !== UNSET_FLAG_VALUE)
|
|
/// @ts-expect-error - flag.value is always string at this moment in time
|
|
castableType = getCastableType(flag.value);
|
|
|
|
if(castableType === 'boolean' && isTypeAssignable(flagType, castableType)) {
|
|
flag.key = newKey;
|
|
flag.config = flagConfig;
|
|
flag.isNegated = true;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @related getCastableType,isTypeAssignable
|
|
* @param {DefaultParserOptsType} opts
|
|
* @param {string} flagVal
|
|
* @param {FlagAny['type']} [expectedType]
|
|
*/
|
|
function runFlagValueTypeResolving(opts, flagVal, expectedType) {
|
|
/** @type {any} */
|
|
let resolvedFlagVal = flagVal;
|
|
|
|
if(expectedType !== undefined && expectedType === 'string')
|
|
return flagVal;
|
|
|
|
/** boolean checking */
|
|
if(flagVal.toLowerCase() === 'true')
|
|
resolvedFlagVal = true;
|
|
else if(flagVal.toLowerCase() === 'false')
|
|
resolvedFlagVal = false;
|
|
else if(opts.resolveFlagValueTypes) {
|
|
/** bigint checking */
|
|
/// @ts-expect-error - isNaN on string
|
|
if(flagVal.length >= 2 && flagVal.endsWith('n') && !isNaN(flagVal.slice(0, -1)))
|
|
resolvedFlagVal = BigInt(flagVal.slice(0, -1));
|
|
/** number & bigint checking */
|
|
/// @ts-expect-error - isNaN on string
|
|
else if(!isNaN(flagVal)) {
|
|
if(expectedType === undefined || expectedType === 'number' || expectedType === 'any')
|
|
resolvedFlagVal = Number(flagVal);
|
|
else if (expectedType !== undefined && expectedType === 'bigint')
|
|
resolvedFlagVal = BigInt(flagVal);
|
|
}
|
|
}
|
|
return resolvedFlagVal;
|
|
}
|
|
|
|
/**
|
|
* @related getCastableType,isTypeAssignable
|
|
* @param {DefaultParserOptsType} opts
|
|
* @param {ReturnType<typeof parseArg>} meta
|
|
* @param {InternalFlagsFlagObj} flag
|
|
* @param {string[]} argv - For the error context
|
|
*/
|
|
function runFlagAssignedValueAssignment(opts, meta, flag, argv) {
|
|
if(flag.config !== undefined && !isTypeAssignable(flag.type, getCastableType(meta.flagAssignedValue))) {
|
|
assignedValueUncastableTypeError(opts, meta, flag, argv);
|
|
return false;
|
|
}
|
|
|
|
const flagVal = runFlagValueTypeResolving(opts, meta.flagAssignedValue, flag.type);
|
|
|
|
if(flag.isNegated && typeof flagVal === 'boolean' && isTypeAssignable(flag.type, 'boolean')) {
|
|
flag.value = !flagVal
|
|
} else {
|
|
flag.value = flagVal;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//#endregion SubworkFunctions
|
|
//#region SecondaryParsers
|
|
|
|
/**
|
|
* @param {string} arg
|
|
* @param {DefaultParserOptsType} opts
|
|
*/
|
|
function parseArg(arg, opts) {
|
|
const argsStr = new StringConsumer(arg);
|
|
|
|
/** return meta flags */
|
|
let isFlag = false;
|
|
/** @type {string} */
|
|
let flagKey = null;
|
|
let minusSignCount = 0;
|
|
|
|
let unescapedEqualSignPos = -1;
|
|
let hasFlagAssignedValue = false;
|
|
//TODO: Add support for comma separated args into arr for flag values
|
|
/** @type {string} */
|
|
let flagAssignedValue = null;
|
|
|
|
|
|
/** parser character flags */
|
|
let isBackslashed = false;
|
|
let isPotentialFlag = false;
|
|
let hasPotentialFlagAssignedValue = false;
|
|
let readingMinusSignCount = true;
|
|
while(argsStr.hasNext()) {
|
|
let char = argsStr.next();
|
|
/** !isBackslashed check redundant here, readingMinusSignCount will always be false if previous character was not a - */
|
|
if(char === '-' && readingMinusSignCount) {
|
|
isPotentialFlag = true;
|
|
minusSignCount++;
|
|
continue;
|
|
}
|
|
|
|
/** Must not be a minus sign */
|
|
readingMinusSignCount = false;
|
|
|
|
/** Flag has an assigned value in the arg*/
|
|
if(char === "=" && isFlag) {
|
|
if(!isBackslashed) {
|
|
if(!hasPotentialFlagAssignedValue) {
|
|
hasPotentialFlagAssignedValue = true;
|
|
unescapedEqualSignPos = argsStr.getPointer()-1;
|
|
continue;
|
|
}
|
|
} else {
|
|
if(hasFlagAssignedValue) {
|
|
/** Consume the backslash */
|
|
flagAssignedValue = flagAssignedValue.slice(0, -1)
|
|
} else {
|
|
/** Consume the backslash */
|
|
flagKey = flagKey.slice(0, -1)
|
|
}
|
|
isBackslashed = false;
|
|
}
|
|
}
|
|
|
|
/** Making sure there is indeed something after the initial - */
|
|
if(isPotentialFlag && !isFlag) {
|
|
isFlag = true;
|
|
flagKey = '';
|
|
}
|
|
|
|
/** Making sure there is indeed something after the = */
|
|
if(hasPotentialFlagAssignedValue && !hasFlagAssignedValue) {
|
|
hasFlagAssignedValue = true;
|
|
flagAssignedValue = '';
|
|
}
|
|
|
|
if(isFlag) {
|
|
/** If is still during flag phase, add flagKey */
|
|
if(!hasFlagAssignedValue) {
|
|
flagKey += char;
|
|
}
|
|
/** Else if it has a flagAssignedValue, add value */
|
|
else {
|
|
flagAssignedValue += char;
|
|
}
|
|
}
|
|
|
|
// Weird and gross handling of backslashes, but explicit, 'defined' (in ./types.d.ts -> ParserOpts.handleBackslashesForSpecialFlagChars) and toggleable
|
|
if(char === '\\') {
|
|
if(opts.handleBackslashesForSpecialFlagChars) {
|
|
if(!isBackslashed) {
|
|
if(isFlag && argsStr.hasNext() && argsStr.peek() === '-') {
|
|
/** Consume the backslash */
|
|
flagKey = flagKey.slice(0, -1)
|
|
}
|
|
else
|
|
isBackslashed = true;
|
|
}
|
|
}
|
|
}
|
|
else if(isBackslashed)
|
|
isBackslashed = false;
|
|
}
|
|
return { isFlag, minusSignCount, flagKey, hasFlagAssignedValue, flagAssignedValue, unescapedEqualSignPos };
|
|
}
|
|
|
|
/**
|
|
* @param {string[]} shorthandFlags
|
|
* @param {string} shorthandString
|
|
*/
|
|
function parseShorthandFlags(shorthandFlags, shorthandString) {
|
|
shorthandFlags = shorthandFlags.concat();
|
|
const shorthandStr = new StringConsumer(shorthandString);
|
|
|
|
let foundFlags = [];
|
|
let currStr = '';
|
|
|
|
while(shorthandStr.hasNext()) {
|
|
let char = shorthandStr.next();
|
|
currStr += char;
|
|
|
|
let perfectMath = shorthandFlags.findIndex((shorthand) => shorthand === currStr);
|
|
let lastIsmMatch = shorthandFlags.findIndex((shorthand) => shorthand === currStr.slice(0, -1));
|
|
|
|
/** Has shorthand that can start with the currentStr but is missing some letters */
|
|
if(shorthandStr.hasNext() && shorthandFlags.some((shorthand) => shorthand.startsWith(currStr) && currStr !== shorthand)) {
|
|
continue;
|
|
} else if(perfectMath !== -1) {
|
|
foundFlags.push(currStr);
|
|
shorthandFlags.splice(perfectMath, 1);
|
|
} else if(lastIsmMatch !== -1) {
|
|
foundFlags.push(currStr.slice(0, -1));
|
|
shorthandFlags.splice(lastIsmMatch, 1);
|
|
if(shorthandStr.hasNext()) {
|
|
currStr = char;
|
|
continue;
|
|
}
|
|
else {
|
|
foundFlags.push(char);
|
|
}
|
|
} else {
|
|
foundFlags.push(...currStr.split(''))
|
|
}
|
|
|
|
currStr = '';
|
|
}
|
|
|
|
return foundFlags;
|
|
}
|
|
|
|
//#endregion SecondaryParsers
|
|
//#region end SecondaryParsers
|
|
//#endregion end SecondaryParsers
|
|
|
|
/**
|
|
* @interface
|
|
* @typedef {{ key: string, value: typeof UNSET_FLAG_VALUE | number | boolean | string, isShorthand: boolean, config: ReturnType<typeof getFlag>, isNegated: boolean, type: FlagAny['type'] }} InternalFlagsFlagObj
|
|
*/
|
|
|
|
// TODO: Support multiple flag as a option = overload input
|
|
// TODO: ^ Add a flag so that if the previous thing is turned off, determine which flag gets picked first (last or first instance, current=last)
|
|
// TODO: Export a function to list all the flags and their values, with a description property on the flags
|
|
|
|
// TODO: Add an option for custom flag setting character, and wether they're stackable or not (so something to allow '/flag' to work)
|
|
|
|
// TODO: Fix typescript types of returned flags to include opts.resolveFlagValueTypes into it as well as checking if it has a default and if not make it an optional?, and type never if it's not in the flags object
|
|
/**
|
|
* @template {FlagsI} Flags
|
|
* @template {{ [K in keyof Flags]: Flags[K]['type'] extends 'boolean' ? boolean : Flags[K]['type'] extends 'number' ? number : Flags[K]['type'] extends 'string' ? string : string | boolean | number }} FlagsReturn
|
|
* @param {string[]} argv
|
|
* @param {ParserOpts<Flags>} opts
|
|
* @returns {{ input: string[], flags: FlagsReturn, unparsed: string[] } }}
|
|
*/
|
|
function parser(argv, opts) {
|
|
if(!Array.isArray(argv) || argv.some((e) => typeof e !== 'string'))
|
|
throw new TypeError(`Argv must be type string[]!`);
|
|
|
|
expectType('opts', 'object', opts, false);
|
|
/** @throwable Will throw if anything is wrong */
|
|
validateAndFillDefaults(opts);
|
|
|
|
if(opts.addFlagShorthandsAsAliases) {
|
|
Object.values(opts.flags).forEach((flagConfig) => {
|
|
if(flagConfig.shorthands.length >= 1)
|
|
/// @ts-ignore
|
|
flagConfig.aliases.push(...flagConfig.shorthands)
|
|
})
|
|
}
|
|
|
|
const allShorthandFlags = getAllShorthandFlags(opts);
|
|
|
|
/** @type {InternalFlagsFlagObj[]} */
|
|
const flags = [];
|
|
/** @type {string[]} */
|
|
const input = [];
|
|
/** @type {string[]} */
|
|
const unparsed = [];
|
|
|
|
/** @readonly Internal value for keeping track previous flag being available for a natural flag value assignment (ie: ['--flag', 'hi'] -> { "flag": 'hi' }) */
|
|
let naturalFlagAssignment = {
|
|
isLookingForValue: false,
|
|
/** @type {FlagAny['type']} */
|
|
requiredType: null
|
|
};
|
|
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const arg = argv[i];
|
|
|
|
const meta = parseArg(arg, opts);
|
|
|
|
/** @type {argMeta} */
|
|
const argMeta = {
|
|
...meta,
|
|
isShorthand: meta.minusSignCount === 1,
|
|
isInput: !meta.isFlag,
|
|
isFlagNaturalValue: false
|
|
}
|
|
if(!meta.isFlag && naturalFlagAssignment.isLookingForValue && isTypeAssignable(naturalFlagAssignment.requiredType, getCastableType(arg))) {
|
|
argMeta.isFlagNaturalValue = true;
|
|
argMeta.isInput = false;
|
|
}
|
|
|
|
//#region Parsing filters
|
|
if(typeof opts.shouldStopParsingFunc === 'function') {
|
|
let shouldStop = false;
|
|
try {
|
|
shouldStop = opts.shouldStopParsingFunc(arg, i, argv, argMeta, input);
|
|
} catch (stopParsingFuncError) {
|
|
const error = new EvalError(`Function opts.shouldStopParsingFunc threw an error!`);
|
|
error.cause = stopParsingFuncError;
|
|
/// @ts-ignore
|
|
error.context = { arg, index: i, argMeta, stopParsingFunc: opts.shouldStopParsingFunc, argv };
|
|
throw error;
|
|
}
|
|
if(shouldStop) {
|
|
unparsed.push(...argv.slice(i));
|
|
break;
|
|
}
|
|
}
|
|
if(typeof opts.shouldStopParsingStr === 'string' && opts.shouldStopParsingStr === arg) {
|
|
unparsed.push(...argv.slice(i))
|
|
break;
|
|
}
|
|
if(typeof opts.parseFilterFunc === 'function') {
|
|
let filtered = false;
|
|
try {
|
|
filtered = opts.parseFilterFunc(arg, i, argv, argMeta, input);
|
|
} catch (parseFilterError) {
|
|
const error = new EvalError(`Function opts.parseFilterFunc threw an error!`);
|
|
error.cause = parseFilterError;
|
|
/// @ts-ignore
|
|
error.context = { arg, index: i, argMeta, parseFilterFunc: opts.parseFilterFunc, argv };
|
|
throw error;
|
|
}
|
|
if(filtered) {
|
|
continue;
|
|
}
|
|
}
|
|
//#endregion Parsing filters
|
|
|
|
if(meta.isFlag) {
|
|
/** If allow singular dashlong flags, just fall through to the full flag handling */
|
|
/** Redundant hasFullFlag given later might getFullFlag but yk easier implementation like this */
|
|
if(argMeta.isShorthand && !(opts.allowSingularDashLongFlags && hasFullFlag(opts, meta.flagKey))) {
|
|
parseShorthandFlags(allShorthandFlags, meta.flagKey)
|
|
.forEach((shorthandKey) => {
|
|
/** @readonly */
|
|
let flagKey = shorthandKey;
|
|
if(opts.lowerCaseFlags)
|
|
flagKey = flagKey.toLowerCase();
|
|
|
|
/** @type {InternalFlagsFlagObj} */
|
|
const flag = { key: flagKey, value: UNSET_FLAG_VALUE, isShorthand: true, config: null, isNegated: false, type: opts.defaultFlagType };
|
|
runAutomaticBooleanFlagNegation(opts, flag);
|
|
assureValidFlagConfigState(opts, flag);
|
|
if(flag.config !== undefined)
|
|
flag.type = flag.config.type;
|
|
|
|
flags.push(flag);
|
|
});
|
|
|
|
const flag = flags.at(-1);
|
|
|
|
if(meta.hasFlagAssignedValue) {
|
|
naturalFlagAssignment = { isLookingForValue: false, requiredType: null };
|
|
runFlagAssignedValueAssignment(opts, meta, flag, argv);
|
|
continue;
|
|
}
|
|
|
|
if(flag.config === undefined || flag.config.acceptsNaturalValue)
|
|
naturalFlagAssignment = { isLookingForValue: true, requiredType: flag.type };
|
|
continue;
|
|
}
|
|
|
|
/** @readonly I like dark blue, plus if is better than a ? assignment from a const */
|
|
let flagKey = meta.flagKey;
|
|
if(opts.lowerCaseFlags)
|
|
flagKey = flagKey.toLowerCase();
|
|
|
|
/** @type {InternalFlagsFlagObj} */
|
|
const flag = { key: flagKey, value: UNSET_FLAG_VALUE, isShorthand: false, config: null, isNegated: false, type: opts.defaultFlagType };
|
|
|
|
runAutomaticBooleanFlagNegation(opts, flag);
|
|
assureValidFlagConfigState(opts, flag);
|
|
if(flag.config !== undefined)
|
|
flag.type = flag.config.type;
|
|
|
|
if(meta.hasFlagAssignedValue) {
|
|
naturalFlagAssignment = { isLookingForValue: false, requiredType: null };
|
|
const succeeded = runFlagAssignedValueAssignment(opts, meta, flag, argv);
|
|
if(succeeded)
|
|
flags.push(flag);
|
|
continue;
|
|
}
|
|
|
|
flags.push(flag);
|
|
if(flag.config === undefined || flag.config.acceptsNaturalValue)
|
|
naturalFlagAssignment = { isLookingForValue: true, requiredType: flag.type };
|
|
continue;
|
|
|
|
}
|
|
else {
|
|
let inputVal = arg;
|
|
|
|
// Hardcoded edge case, not a fan, but if it has more than one backslash, it was never gonna be a flag anyways
|
|
if(opts.handleBackslashesForSpecialFlagChars && inputVal.startsWith('\\-'))
|
|
inputVal = inputVal.slice(1);
|
|
|
|
if(!naturalFlagAssignment.isLookingForValue) {
|
|
if(opts.lowerCaseInputValues)
|
|
inputVal = inputVal.toLowerCase();
|
|
input.push(inputVal);
|
|
continue
|
|
}
|
|
|
|
const flag = flags.at(-1);
|
|
|
|
if(isTypeAssignable(flag.type, getCastableType(inputVal))) {
|
|
const flagVal = runFlagValueTypeResolving(opts, inputVal, flag.type);
|
|
|
|
if(flag.isNegated && typeof flagVal === 'boolean' && isTypeAssignable(flag.type, 'boolean')) {
|
|
flag.value = !flagVal
|
|
} else {
|
|
flag.value = flagVal;
|
|
}
|
|
}
|
|
else {
|
|
if(opts.lowerCaseInputValues)
|
|
inputVal = inputVal.toLowerCase();
|
|
input.push(inputVal);
|
|
}
|
|
naturalFlagAssignment = { isLookingForValue: false, requiredType: null };
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const flagReturnObj = {};
|
|
/** Some (not all, some is run in previous loop) Flag processing (like checking types, which we can't run in the argv.forEach) and return object building */
|
|
flags.forEach((flag) => {
|
|
if(flag.value === UNSET_FLAG_VALUE) {
|
|
if(flag.config !== undefined) {
|
|
if(flag.config.requiredValue === true) {
|
|
const message = `Flag "${flag.config.name}" is missing a required value of type "${flag.config.type}"!`;
|
|
const error = new InputError(message);
|
|
error.context = { flag: flag.config, flagType: flag.config.type, flagMatch: flag.key, value: UNSET_FLAG_VALUE, argv };
|
|
return giveError(opts, {
|
|
error,
|
|
message: message + `\n - Flag: "${flag.config.name}", match: "${flag.key}", value: UNSET_FLAG_VALUE, type: "${flag.config.type}"`,
|
|
exitCode: 1
|
|
});
|
|
}
|
|
/** Set default except booleans (since this function only runs if the flag is present, and a boolean flag being present is by itself a true value (unless negated)) */
|
|
if(typeof flag.config.default !== 'undefined' && flag.config.type !== 'boolean')
|
|
flag.value = flag.config.default;
|
|
}
|
|
if(flag.value === UNSET_FLAG_VALUE) {
|
|
switch(flag.type) {
|
|
case 'any':
|
|
case 'boolean':
|
|
/** ProCoder: flag.value = !flag.isNegated */
|
|
flag.value = true;
|
|
if(flag.isNegated)
|
|
flag.value = false;
|
|
break;
|
|
case "bigint":
|
|
case "number":
|
|
case "string":
|
|
flag.value = null;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
if(typeof flag.value === 'string')
|
|
flag.value = runFlagValueTypeResolving(opts, flag.value, flag.type);
|
|
if(typeof flag.value === 'string' && opts.lowerCaseFlagValues)
|
|
flag.value = flag.value.toLowerCase()
|
|
}
|
|
|
|
if(flag.config === undefined && !opts.allowUnknownFlags)
|
|
return unknownFlagError(opts, flag.key, flag.value, argv);
|
|
|
|
const flagName = flag.config?.name || flag.key;
|
|
|
|
flagReturnObj[flagName] = flag.value;
|
|
})
|
|
|
|
/** Setting missing flags */
|
|
Object.entries(opts.flags).forEach(([flagName, flagConfig]) => {
|
|
if(typeof flagReturnObj[flagName] !== 'undefined')
|
|
return;
|
|
/** Not a needed check as you can tell, but present to explicitly highlight that behavior in the code */
|
|
if(typeof flagConfig.default === 'undefined')
|
|
return flagReturnObj[flagName] = undefined;
|
|
flagReturnObj[flagName] = flagConfig.default;
|
|
});
|
|
|
|
/// @ts-ignore - falsly wrong flags type
|
|
return { flags: flagReturnObj, input, unparsed };
|
|
}
|
|
|
|
module.exports = { parser, InputError }; |