Added bigint flag type & negated flag bug fixes due to flag.requiredValue option, some refactors for run<Thing> & some other internal functions & fixed 'ignore' value for opts.behaviorOnInputError & fixed flag values on string typed flags when opts.resolveFlagValueTypes was turned on
This commit is contained in:
parent
aef5a62455
commit
ef73c447df
186
index.js
186
index.js
@ -31,6 +31,13 @@ const { ok } = require('node:assert');
|
||||
* @typedef {import('./types.d.ts').FlagAny} FlagAny
|
||||
*/
|
||||
|
||||
class InputError extends Error {
|
||||
context = null;
|
||||
};
|
||||
|
||||
class UnsetFlagValueType {}
|
||||
const UNSET_FLAG_VALUE = new UnsetFlagValueType();
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {JSTypes} type
|
||||
@ -90,10 +97,10 @@ function namedValidateOrDefaultIfUnset(objName, obj, keyName, keyType, defaultVa
|
||||
*/
|
||||
function validateAndFillDefaults(opts) {
|
||||
|
||||
const allowedTypeofDefaultType = ["string", "number", "boolean"];
|
||||
const allowedTypeofDefaultType = ["string", "number", "bigint", "boolean"];
|
||||
const validFlagTypes = ['any', ...allowedTypeofDefaultType];
|
||||
|
||||
const allowedBehaviorOnInputErrorTypes = ['throw', 'log', 'log&exit'];
|
||||
const allowedBehaviorOnInputErrorTypes = ['throw', 'log', 'log&exit', 'ignore'];
|
||||
|
||||
validateOrDefaultIfUnset(opts, 'warningLogger', 'function', console.log);
|
||||
validateOrDefaultIfUnset(opts, 'behaviorOnInputError', 'string', 'throw');
|
||||
@ -296,21 +303,20 @@ function getShorthandFlag(opts, flagKeyInput) {
|
||||
/**
|
||||
* @related runFlagValueTypeResolving
|
||||
* @param {string} input
|
||||
* @returns {"number"|"boolean"|"string"}
|
||||
* @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';
|
||||
}
|
||||
|
||||
class InputError extends Error {
|
||||
context = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {DefaultParserOptsType} opts
|
||||
* @param {{ error: InputError | Error, message: string, exitCode: number }} errorObj
|
||||
@ -347,7 +353,9 @@ function isAssignedValueTypeAssignable(opts, meta, flag, argv) {
|
||||
}
|
||||
|
||||
const castableType = getCastableType(meta.flagAssignedValue);
|
||||
if(flagConfig !== undefined && flagConfig.type !== 'any' && flagConfig.type !== castableType) {
|
||||
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 };
|
||||
@ -403,60 +411,113 @@ function getAllShorthandFlags(opts) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @modifies flag
|
||||
* And uh, yeah bit of a heavy refactor given it's a biggggg chicken and egg situation, where I need the type of the flag value (assigned or given or default), but to get the
|
||||
* proper flag config, I first need to run the negated boolean flag, so uh yeah, need to run allat twice :D
|
||||
* ### To be run before any flag value assignments (flag.value always expected to be string)
|
||||
* @param {DefaultParserOptsType} opts
|
||||
* @param {string} flagKey
|
||||
* @param {any} flagVal
|
||||
* @param {InternalFlagsFlagObj} flag
|
||||
*/
|
||||
function runFullFlagAutomaticBooleanNegation(opts, flagKey, flagVal) {
|
||||
/** Make sure flag isn't set in our vals */
|
||||
if(!hasFullFlag(opts, flagKey) && opts.automaticBooleanFlagNegation.enabled && typeof flagVal === 'boolean' && flagKey.length > 2) {
|
||||
if(flagKey.length > 3 && flagKey.toLowerCase().startsWith('no-')) {
|
||||
flagKey = flagKey.slice(3);
|
||||
flagVal = !flagVal;
|
||||
}
|
||||
else if(opts.automaticBooleanFlagNegation.allowUnspacedNegatedFlags && flagKey.toLowerCase().startsWith('no')) {
|
||||
flagKey = flagKey.slice(2);
|
||||
flagVal = !flagVal;
|
||||
}
|
||||
}
|
||||
return { flagKey, flagVal }
|
||||
}
|
||||
function runAutomaticBooleanFlagNegation(opts, flag) {
|
||||
if(opts.automaticBooleanFlagNegation.enabled !== true)
|
||||
return false;
|
||||
if(flag.isShorthand && opts.automaticBooleanFlagNegation.shorthandNegation !== true)
|
||||
return false;
|
||||
|
||||
/**
|
||||
* @param {DefaultParserOptsType} opts
|
||||
* @param {string} flagKey
|
||||
* @param {any} flagVal
|
||||
*/
|
||||
function runShorthandFlagAutomaticBooleanNegation(opts, flagKey, flagVal) {
|
||||
/** @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, flagKey) && opts.automaticBooleanFlagNegation.enabled && opts.automaticBooleanFlagNegation.shorthandNegation && typeof flagVal === 'boolean' && flagKey.length >= 2) {
|
||||
if(opts.automaticBooleanFlagNegation.allowUnspacedNegatedFlags && flagKey.toLowerCase().startsWith('n')) {
|
||||
flagKey = flagKey.slice(1);
|
||||
flagVal = !flagVal;
|
||||
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);
|
||||
}
|
||||
}
|
||||
return { flagKey, flagVal }
|
||||
} 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) {
|
||||
/** @readonly */
|
||||
let flagType = opts.defaultFlagType;
|
||||
if(typeof flagConfig !== 'undefined')
|
||||
flagType = flagConfig.type;
|
||||
|
||||
/** @type {ReturnType<typeof getCastableType>} */
|
||||
let castableType = 'boolean';
|
||||
if(flag.value !== UNSET_FLAG_VALUE)
|
||||
/// @ts-ignore
|
||||
castableType = getCastableType(flag.value);
|
||||
|
||||
if((flagType === 'boolean' || flagType === 'any') && castableType === 'boolean') {
|
||||
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;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @related getCastableType
|
||||
* ### Warning: This does not check the flag type despite passing in a flagConfig!
|
||||
* @param {DefaultParserOptsType} opts
|
||||
* @param {string} flagKey
|
||||
* @param {any} flagVal
|
||||
* @param {string} flagVal
|
||||
* @param {ReturnType<typeof getFlag>} flagConfig
|
||||
*/
|
||||
function runFlagValueTypeResolving(opts, flagKey, flagVal) {
|
||||
function runFlagValueTypeResolving(opts, flagVal, flagConfig) {
|
||||
/** @type {any} */
|
||||
let resolvedFlagVal = flagVal;
|
||||
|
||||
/** boolean checking */
|
||||
if(flagVal.toLowerCase() === 'true')
|
||||
flagVal = true;
|
||||
resolvedFlagVal = true;
|
||||
else if(flagVal.toLowerCase() === 'false')
|
||||
flagVal = false;
|
||||
resolvedFlagVal = false;
|
||||
else if(opts.resolveFlagValueTypes) {
|
||||
/** number checking */
|
||||
/** bigint checking */
|
||||
/// @ts-ignore
|
||||
if(!isNaN(flagVal))
|
||||
flagVal = Number(flagVal);
|
||||
if(flagVal.length >= 2 && flagVal.endsWith('n') && !isNaN(flagVal.slice(0, -1)))
|
||||
resolvedFlagVal = BigInt(flagVal.slice(0, -1));
|
||||
/** number & bigint checking */
|
||||
/// @ts-ignore
|
||||
else if(!isNaN(flagVal)) {
|
||||
if(typeof flagConfig === 'undefined' || flagConfig.type === 'number' || flagConfig.type === 'any')
|
||||
resolvedFlagVal = Number(flagVal);
|
||||
else if (typeof flagConfig !== 'undefined' && flagConfig.type === 'bigint')
|
||||
resolvedFlagVal = BigInt(flagVal);
|
||||
}
|
||||
return { flagVal };
|
||||
}
|
||||
return resolvedFlagVal;
|
||||
}
|
||||
|
||||
//#endregion SubworkFunctions
|
||||
@ -586,9 +647,6 @@ function parseShorthandFlags(shorthandFlags, shorthandString) {
|
||||
//#region end SecondaryParsers
|
||||
//#endregion end SecondaryParsers
|
||||
|
||||
class UnsetFlagValueType {}
|
||||
const UNSET_FLAG_VALUE = new UnsetFlagValueType();
|
||||
|
||||
/**
|
||||
* @interface
|
||||
* @typedef {{ key: string, value: typeof UNSET_FLAG_VALUE | number | boolean | string, isShorthand: boolean, lookedupFlagConfig: boolean, flagConfig: ReturnType<typeof getFlag> }} InternalFlagsFlagObj
|
||||
@ -689,9 +747,9 @@ function parser(argv, opts) {
|
||||
if(flagConfig !== undefined && flagConfig.type !== undefined)
|
||||
flagType = flagConfig.type;
|
||||
|
||||
const { flagVal } = runFlagValueTypeResolving(opts, flag.key, arg);
|
||||
const castableType = getCastableType(arg);
|
||||
|
||||
if(flagType === typeof flagVal || flagType === 'any')
|
||||
if(['string', 'any', castableType].includes(flagType) || (castableType === 'number' && flagType === 'bigint'))
|
||||
flag.value = arg;
|
||||
else
|
||||
input.push(arg);
|
||||
@ -707,6 +765,9 @@ function parser(argv, opts) {
|
||||
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);
|
||||
|
||||
/** Process some flag values */
|
||||
let { key: flagKey, value: flagVal } = flag;
|
||||
|
||||
@ -722,10 +783,9 @@ function parser(argv, opts) {
|
||||
ok(flagConfig !== null, 'flagConfig should always be undefined if not found, and would only be in the wrong state of null if not reassigned at a later point (which should be impossible');
|
||||
|
||||
if(flagVal === UNSET_FLAG_VALUE) {
|
||||
/** Setting defaults */
|
||||
/** @type {number|string|boolean} */
|
||||
flagVal = true;
|
||||
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}"!`;
|
||||
const error = new InputError(message);
|
||||
@ -740,15 +800,23 @@ function parser(argv, opts) {
|
||||
if(typeof flagConfig.default !== 'undefined' && flagConfig.type !== 'boolean')
|
||||
flagVal = flagConfig.default;
|
||||
}
|
||||
if(flagVal === UNSET_FLAG_VALUE) {
|
||||
switch(flagType) {
|
||||
case 'any':
|
||||
case 'boolean':
|
||||
flagVal = true;
|
||||
break;
|
||||
case "bigint":
|
||||
case "number":
|
||||
case "string":
|
||||
flagVal = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(typeof flagVal === 'string')
|
||||
({ flagVal } = runFlagValueTypeResolving(opts, flagKey, flagVal));
|
||||
/** Only on type flagVal */
|
||||
if(flag.isShorthand)
|
||||
({ flagKey, flagVal } = runShorthandFlagAutomaticBooleanNegation(opts, flagKey, flagVal));
|
||||
else
|
||||
({ flagKey, flagVal } = runFullFlagAutomaticBooleanNegation(opts, flagKey, flagVal));
|
||||
flagVal = runFlagValueTypeResolving(opts, flagVal, flagConfig);
|
||||
|
||||
|
||||
flag.key = flagKey;
|
||||
@ -764,7 +832,7 @@ function parser(argv, opts) {
|
||||
flagReturnObj[realFlagKey] = flag.value;
|
||||
})
|
||||
|
||||
/** Setting defaults if missing */
|
||||
/** Setting missing flags */
|
||||
Object.entries(opts.flags).forEach(([flagName, flagConfig]) => {
|
||||
if(typeof flagReturnObj[flagName] !== 'undefined')
|
||||
return;
|
||||
|
||||
4
types.d.ts
vendored
4
types.d.ts
vendored
@ -9,7 +9,7 @@ interface FlagT<T, Z> {
|
||||
requiredValue?: boolean
|
||||
};
|
||||
|
||||
export type FlagAny = FlagT<string, "string"> | FlagT<boolean, "boolean"> | FlagT<number, "number"> | FlagT<string | number | boolean, "any">
|
||||
export type FlagAny = FlagT<string, "string"> | FlagT<boolean, "boolean"> | FlagT<number, "number"> | FlagT<bigint, "bigint"> | FlagT<string | number | bigint | boolean, "any">
|
||||
|
||||
export interface FlagsI { [key: readonly string]: FlagAny }
|
||||
|
||||
@ -47,7 +47,7 @@ export interface ParserOpts<Flags extends FlagsI> {
|
||||
/** Override to change how warnings are emitted @default console.warn */
|
||||
warningLogger?: (log: string) => void;
|
||||
/** Default type for either unknown flags or flags without an explicit type being set @default "any" */
|
||||
defaultFlagType?: "string" | "boolean" | "number" | "any";
|
||||
defaultFlagType?: "string" | "boolean" | "bigint" | "number" | "any";
|
||||
/**
|
||||
* Behavior when input does not follow the provided flags constraints (eg: flag assigned value (--flag=value) not being the correct type).
|
||||
*
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user