args-tokens
Version:
parseArgs tokens compatibility and more high-performance parser
283 lines (281 loc) • 11.2 kB
JavaScript
import { hasLongOptionPrefix, isShortOption } from "./parser-M-ayhS1h.js";
import { kebabnize } from "./utils-1LQrGCWG.js";
//#region src/resolver.ts
const SKIP_POSITIONAL_DEFAULT = -1;
/**
* Resolve command line arguments.
*
* @typeParam A - {@link Args | Arguments}, which is an object that defines the command line arguments.
*
* @param args - An arguments that contains {@link ArgSchema | arguments schema}.
* @param tokens - An array of {@link ArgToken | tokens}.
* @param resolveArgs - An arguments that contains {@link ResolveArgs | resolve arguments}.
* @returns An object that contains the values of the arguments, positional arguments, rest arguments, {@link AggregateError | validation errors}, and explicit provision status.
*
* @example
* ```typescript
* // passed tokens: --port 3000
*
* const { values, explicit } = resolveArgs({
* port: {
* type: 'number',
* default: 8080
* },
* host: {
* type: 'string',
* default: 'localhost'
* }
* }, parsedTokens)
*
* values.port // 3000
* values.host // 'localhost'
*
* explicit.port // true (explicitly provided)
* explicit.host // false (not provided, fallback to default)
* ```
*/
function resolveArgs(args, tokens, { shortGrouping = false, skipPositional = SKIP_POSITIONAL_DEFAULT, toKebab = false } = {}) {
const skipPositionalIndex = typeof skipPositional === "number" ? Math.max(skipPositional, SKIP_POSITIONAL_DEFAULT) : SKIP_POSITIONAL_DEFAULT;
const rest = [];
const optionTokens = [];
const positionalTokens = [];
let currentLongOption;
let currentShortOption;
const expandableShortOptions = [];
function toShortValue() {
if (expandableShortOptions.length === 0) return void 0;
else {
const value = expandableShortOptions.map((token) => token.name).join("");
expandableShortOptions.length = 0;
return value;
}
}
function applyLongOptionValue(value = void 0) {
if (currentLongOption) {
currentLongOption.value = value;
optionTokens.push({ ...currentLongOption });
currentLongOption = void 0;
}
}
function applyShortOptionValue(value = void 0) {
if (currentShortOption) {
currentShortOption.value = value || toShortValue();
optionTokens.push({ ...currentShortOption });
currentShortOption = void 0;
}
}
/**
* analyze phase to resolve value
* separate tokens into positionals, long and short options, after that resolve values
*/
const schemas = Object.values(args);
let terminated = false;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.kind === "positional") {
if (terminated && token.value) {
rest.push(token.value);
continue;
}
if (currentShortOption) {
const found = schemas.find((schema) => schema.short === currentShortOption.name && schema.type === "boolean");
if (found) positionalTokens.push({ ...token });
} else if (currentLongOption) {
const found = args[currentLongOption.name]?.type === "boolean";
if (found) positionalTokens.push({ ...token });
} else positionalTokens.push({ ...token });
applyLongOptionValue(token.value);
applyShortOptionValue(token.value);
} else if (token.kind === "option") if (token.rawName) {
if (hasLongOptionPrefix(token.rawName)) {
applyLongOptionValue();
if (token.inlineValue) optionTokens.push({ ...token });
else currentLongOption = { ...token };
applyShortOptionValue();
} else if (isShortOption(token.rawName)) if (currentShortOption) {
if (currentShortOption.index === token.index) if (shortGrouping) {
currentShortOption.value = token.value;
optionTokens.push({ ...currentShortOption });
currentShortOption = { ...token };
} else expandableShortOptions.push({ ...token });
else {
currentShortOption.value = toShortValue();
optionTokens.push({ ...currentShortOption });
currentShortOption = { ...token };
}
applyLongOptionValue();
} else {
currentShortOption = { ...token };
applyLongOptionValue();
}
} else {
if (currentShortOption && currentShortOption.index == token.index && token.inlineValue) {
currentShortOption.value = token.value;
optionTokens.push({ ...currentShortOption });
currentShortOption = void 0;
}
applyLongOptionValue();
}
else {
if (token.kind === "option-terminator") terminated = true;
applyLongOptionValue();
applyShortOptionValue();
}
}
/**
* check if the last long or short option is not resolved
*/
applyLongOptionValue();
applyShortOptionValue();
/**
* resolve values
*/
const values = Object.create(null);
const errors = [];
const explicit = Object.create(null);
const actualInputNames = /* @__PURE__ */ new Map();
function checkTokenName(option, schema, token) {
return token.name === (schema.type === "boolean" ? schema.negatable && token.name?.startsWith("no-") ? `no-${option}` : option : option);
}
const positionalItemCount = tokens.filter((token) => token.kind === "positional").length;
function getPositionalSkipIndex() {
return Math.min(skipPositionalIndex, positionalItemCount);
}
let positionalsCount = 0;
for (const [rawArg, schema] of Object.entries(args)) {
const arg = toKebab || schema.toKebab ? kebabnize(rawArg) : rawArg;
explicit[rawArg] = false;
if (schema.type === "positional") {
if (skipPositionalIndex > SKIP_POSITIONAL_DEFAULT) while (positionalsCount <= getPositionalSkipIndex()) positionalsCount++;
if (schema.multiple) {
const remainingPositionals = positionalTokens.slice(positionalsCount);
if (remainingPositionals.length > 0) {
values[rawArg] = remainingPositionals.map((p) => p.value);
positionalsCount += remainingPositionals.length;
} else if (schema.required) errors.push(createRequireError(arg, schema));
} else {
const positional = positionalTokens[positionalsCount];
if (positional != null) values[rawArg] = positional.value;
else errors.push(createRequireError(arg, schema));
positionalsCount++;
}
continue;
}
if (schema.required) {
const found = optionTokens.find((token) => {
return schema.short && token.name === schema.short || token.rawName && hasLongOptionPrefix(token.rawName) && token.name === arg;
});
if (!found) {
errors.push(createRequireError(arg, schema));
continue;
}
}
for (let i = 0; i < optionTokens.length; i++) {
const token = optionTokens[i];
if (checkTokenName(arg, schema, token) && token.rawName != void 0 && hasLongOptionPrefix(token.rawName) || schema.short === token.name && token.rawName != void 0 && isShortOption(token.rawName)) {
const invalid = validateRequire(token, arg, schema);
if (invalid) {
errors.push(invalid);
continue;
}
explicit[rawArg] = true;
const actualInputName = isShortOption(token.rawName) ? `-${token.name}` : `--${arg}`;
actualInputNames.set(rawArg, actualInputName);
if (schema.type === "boolean") token.value = void 0;
const [parsedValue, error] = parse(token, arg, schema);
if (error) errors.push(error);
else if (schema.multiple) {
values[rawArg] ||= [];
values[rawArg].push(parsedValue);
} else values[rawArg] = parsedValue;
}
}
if (values[rawArg] == null && schema.default != null) values[rawArg] = schema.default;
}
const conflictErrors = checkConflicts(args, explicit, toKebab, actualInputNames);
errors.push(...conflictErrors);
return {
values,
positionals: positionalTokens.map((token) => token.value),
rest,
error: errors.length > 0 ? new AggregateError(errors) : void 0,
explicit
};
}
function parse(token, option, schema) {
switch (schema.type) {
case "string": return typeof token.value === "string" ? [token.value || schema.default, void 0] : [void 0, createTypeError(option, schema)];
case "boolean": return token.value ? [token.value || schema.default, void 0] : [!(schema.negatable && token.name.startsWith("no-")), void 0];
case "number":
if (!isNumeric(token.value)) return [void 0, createTypeError(option, schema)];
return token.value ? [+token.value, void 0] : [+(schema.default || ""), void 0];
case "enum":
if (schema.choices && !schema.choices.includes(token.value)) return [void 0, new ArgResolveError(`Optional argument '--${option}' ${schema.short ? `or '-${schema.short}' ` : ""}should be chosen from '${schema.type}' [${schema.choices.map((c) => JSON.stringify(c)).join(", ")}] values`, option, "type", schema)];
return [token.value || schema.default, void 0];
case "custom":
if (typeof schema.parse !== "function") throw new TypeError(`argument '${option}' should have a 'parse' function`);
try {
return [schema.parse(token.value || String(schema.default || "")), void 0];
} catch (error) {
return [void 0, error];
}
default: throw new Error(`Unsupported argument type '${schema.type}' for option '${option}'`);
}
}
function createRequireError(option, schema) {
const message = schema.type === "positional" ? `Positional argument '${option}' is required` : `Optional argument '--${option}' ${schema.short ? `or '-${schema.short}' ` : ""}is required`;
return new ArgResolveError(message, option, "required", schema);
}
/**
* An error that occurs when resolving arguments.
* This error is thrown when the argument is not valid.
*/
var ArgResolveError = class extends Error {
name;
schema;
type;
/**
* Create an `ArgResolveError` instance.
*
* @param message - the error message
* @param name - the name of the argument
* @param type - the type of the error, either 'type' or 'required'
* @param schema - the argument schema that caused the error
*/
constructor(message, name, type, schema) {
super(message);
this.name = name;
this.type = type;
this.schema = schema;
}
};
function validateRequire(token, option, schema) {
if (schema.required && schema.type !== "boolean" && !token.value) return createRequireError(option, schema);
}
function isNumeric(str) {
return str.trim() !== "" && !isNaN(str);
}
function createTypeError(option, schema) {
return new ArgResolveError(`Optional argument '--${option}' ${schema.short ? `or '-${schema.short}' ` : ""}should be '${schema.type}'`, option, "type", schema);
}
function checkConflicts(args, explicit, toKebab, actualInputNames) {
for (const rawArg in args) {
const schema = args[rawArg];
if (!explicit[rawArg]) continue;
if (!schema.conflicts) continue;
const conflicts = Array.isArray(schema.conflicts) ? schema.conflicts : [schema.conflicts];
for (let i = 0; i < conflicts.length; i++) {
const conflictingArg = conflicts[i];
if (!explicit[conflictingArg]) continue;
const arg = toKebab || schema.toKebab ? kebabnize(rawArg) : rawArg;
const conflictingArgKebab = toKebab || args[conflictingArg]?.toKebab ? kebabnize(conflictingArg) : conflictingArg;
const optionActualName = actualInputNames.get(rawArg) || `--${arg}`;
const conflictingActualName = actualInputNames.get(conflictingArg) || `--${conflictingArgKebab}`;
const message = `Optional argument '${optionActualName}' conflicts with '${conflictingActualName}'`;
return [new ArgResolveError(message, rawArg, "conflict", schema)];
}
}
return [];
}
//#endregion
export { ArgResolveError, resolveArgs };