UNPKG

args-tokens

Version:

parseArgs tokens compatibility and more high-performance parser

283 lines (281 loc) 11.2 kB
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 };