UNPKG

@optique/core

Version:

Type-safe combinatorial command-line interface parser

1,586 lines (1,585 loc) 51.7 kB
import { message, metavar, optionName, optionNames, text, values } from "./message.js"; import { normalizeUsage } from "./usage.js"; import { isValueParser } from "./valueparser.js"; //#region src/parser.ts /** * Creates a parser that always succeeds without consuming any input and * produces a constant value of the type {@link T}. * @template T The type of the constant value produced by the parser. */ function constant(value) { return { $valueType: [], $stateType: [], priority: 0, usage: [], initialState: value, parse(context) { return { success: true, next: context, consumed: [] }; }, complete(state) { return { success: true, value: state }; }, getDocFragments(_state, _defaultValue) { return { fragments: [] }; } }; } function option(...args) { const lastArg = args.at(-1); const secondLastArg = args.at(-2); let valueParser; let optionNames$1; let options = {}; if (isValueParser(lastArg)) { valueParser = lastArg; optionNames$1 = args.slice(0, -1); } else if (typeof lastArg === "object" && lastArg != null) { options = lastArg; if (isValueParser(secondLastArg)) { valueParser = secondLastArg; optionNames$1 = args.slice(0, -2); } else { valueParser = void 0; optionNames$1 = args.slice(0, -1); } } else { optionNames$1 = args; valueParser = void 0; } return { $valueType: [], $stateType: [], priority: 10, usage: [valueParser == null ? { type: "optional", terms: [{ type: "option", names: optionNames$1 }] } : { type: "option", names: optionNames$1, metavar: valueParser.metavar }], initialState: valueParser == null ? { success: true, value: false } : { success: false, error: message`Missing option ${optionNames(optionNames$1)}.` }, parse(context) { if (context.optionsTerminated) return { success: false, consumed: 0, error: message`No more options can be parsed.` }; else if (context.buffer.length < 1) return { success: false, consumed: 0, error: message`Expected an option, but got end of input.` }; if (context.buffer[0] === "--") return { success: true, next: { ...context, buffer: context.buffer.slice(1), state: context.state, optionsTerminated: true }, consumed: context.buffer.slice(0, 1) }; if (optionNames$1.includes(context.buffer[0])) { if (context.state.success && (valueParser != null || context.state.value)) return { success: false, consumed: 1, error: message`${context.buffer[0]} cannot be used multiple times.` }; if (valueParser == null) return { success: true, next: { ...context, state: { success: true, value: true }, buffer: context.buffer.slice(1) }, consumed: context.buffer.slice(0, 1) }; if (context.buffer.length < 2) return { success: false, consumed: 1, error: message`Option ${optionName(context.buffer[0])} requires a value, but got no value.` }; const result = valueParser.parse(context.buffer[1]); return { success: true, next: { ...context, state: result, buffer: context.buffer.slice(2) }, consumed: context.buffer.slice(0, 2) }; } const prefixes = optionNames$1.filter((name) => name.startsWith("--") || name.startsWith("/")).map((name) => name.startsWith("/") ? `${name}:` : `${name}=`); for (const prefix of prefixes) { if (!context.buffer[0].startsWith(prefix)) continue; if (context.state.success && (valueParser != null || context.state.value)) return { success: false, consumed: 1, error: message`${optionName(prefix)} cannot be used multiple times.` }; const value = context.buffer[0].slice(prefix.length); if (valueParser == null) return { success: false, consumed: 1, error: message`Option ${optionName(prefix)} is a Boolean flag, but got a value: ${value}.` }; const result = valueParser.parse(value); return { success: true, next: { ...context, state: result, buffer: context.buffer.slice(1) }, consumed: context.buffer.slice(0, 1) }; } if (valueParser == null) { const shortOptions = optionNames$1.filter((name) => name.match(/^-[^-]$/)); for (const shortOption of shortOptions) { if (!context.buffer[0].startsWith(shortOption)) continue; if (context.state.success && (valueParser != null || context.state.value)) return { success: false, consumed: 1, error: message`${optionName(shortOption)} cannot be used multiple times.` }; return { success: true, next: { ...context, state: { success: true, value: true }, buffer: [`-${context.buffer[0].slice(2)}`, ...context.buffer.slice(1)] }, consumed: [context.buffer[0].slice(0, 2)] }; } } return { success: false, consumed: 0, error: message`No matched option for ${optionName(context.buffer[0])}.` }; }, complete(state) { if (state == null) return valueParser == null ? { success: true, value: false } : { success: false, error: message`Missing option ${optionNames(optionNames$1)}.` }; if (state.success) return state; return { success: false, error: message`${optionNames(optionNames$1)}: ${state.error}` }; }, getDocFragments(_state, defaultValue) { const fragments = [{ type: "entry", term: { type: "option", names: optionNames$1, metavar: valueParser?.metavar }, description: options.description, default: defaultValue != null && valueParser != null ? valueParser.format(defaultValue) : void 0 }]; return { fragments, description: options.description }; }, [Symbol.for("Deno.customInspect")]() { return `option(${optionNames$1.map((o) => JSON.stringify(o)).join(", ")})`; } }; } /** * Creates a parser for command-line flags that must be explicitly provided. * Unlike {@link option}, this parser fails if the flag is not present, making * it suitable for required boolean flags that don't have a meaningful default. * * The key difference from {@link option} is: * - {@link option} without a value parser: Returns `false` when not present * - {@link flag}: Fails parsing when not present, only produces `true` * * This is useful for dependent options where the presence of a flag changes * the shape of the result type. * * @param args The {@link OptionName}s to parse, followed by an optional * {@link FlagOptions} object that allows you to specify * a description or other metadata. * @returns A {@link Parser} that produces `true` when the flag is present * and fails when it is not present. * * @example * ```typescript * // Basic flag usage * const parser = flag("-f", "--force"); * // Succeeds with true: parse(parser, ["-f"]) * // Fails: parse(parser, []) * * // With description * const verboseFlag = flag("-v", "--verbose", { * description: "Enable verbose output" * }); * ``` */ function flag(...args) { const lastArg = args.at(-1); let optionNames$1; let options = {}; if (typeof lastArg === "object" && lastArg != null && !Array.isArray(lastArg)) { options = lastArg; optionNames$1 = args.slice(0, -1); } else optionNames$1 = args; return { $valueType: [], $stateType: [], priority: 10, usage: [{ type: "option", names: optionNames$1 }], initialState: void 0, parse(context) { if (context.optionsTerminated) return { success: false, consumed: 0, error: message`No more options can be parsed.` }; else if (context.buffer.length < 1) return { success: false, consumed: 0, error: message`Expected an option, but got end of input.` }; if (context.buffer[0] === "--") return { success: true, next: { ...context, buffer: context.buffer.slice(1), state: context.state, optionsTerminated: true }, consumed: context.buffer.slice(0, 1) }; if (optionNames$1.includes(context.buffer[0])) { if (context.state?.success) return { success: false, consumed: 1, error: message`${optionName(context.buffer[0])} cannot be used multiple times.` }; return { success: true, next: { ...context, state: { success: true, value: true }, buffer: context.buffer.slice(1) }, consumed: context.buffer.slice(0, 1) }; } const prefixes = optionNames$1.filter((name) => name.startsWith("--") || name.startsWith("/")).map((name) => name.startsWith("/") ? `${name}:` : `${name}=`); for (const prefix of prefixes) if (context.buffer[0].startsWith(prefix)) { const value = context.buffer[0].slice(prefix.length); return { success: false, consumed: 1, error: message`Flag ${optionName(prefix.slice(0, -1))} does not accept a value, but got: ${value}.` }; } const shortOptions = optionNames$1.filter((name) => name.match(/^-[^-]$/)); for (const shortOption of shortOptions) { if (!context.buffer[0].startsWith(shortOption)) continue; if (context.state?.success) return { success: false, consumed: 1, error: message`${optionName(shortOption)} cannot be used multiple times.` }; return { success: true, next: { ...context, state: { success: true, value: true }, buffer: [`-${context.buffer[0].slice(2)}`, ...context.buffer.slice(1)] }, consumed: [context.buffer[0].slice(0, 2)] }; } return { success: false, consumed: 0, error: message`No matched option for ${optionName(context.buffer[0])}.` }; }, complete(state) { if (state == null) return { success: false, error: message`Required flag ${optionNames(optionNames$1)} is missing.` }; if (state.success) return { success: true, value: true }; return { success: false, error: message`${optionNames(optionNames$1)}: ${state.error}` }; }, getDocFragments(_state, _defaultValue) { const fragments = [{ type: "entry", term: { type: "option", names: optionNames$1 }, description: options.description }]; return { fragments, description: options.description }; }, [Symbol.for("Deno.customInspect")]() { return `flag(${optionNames$1.map((o) => JSON.stringify(o)).join(", ")})`; } }; } /** * Creates a parser that expects a single argument value. * This parser is typically used for positional arguments * that are not options or flags. * @template T The type of the value produced by the parser. * @param valueParser The {@link ValueParser} that defines how to parse * the argument value. * @param options Optional configuration for the argument parser, * allowing you to specify a description or other metadata. * @returns A {@link Parser} that expects a single argument value and produces * the parsed value of type {@link T}. */ function argument(valueParser, options = {}) { const optionPattern = /^--?[a-z0-9-]+$/i; const term = { type: "argument", metavar: valueParser.metavar }; return { $valueType: [], $stateType: [], priority: 5, usage: [term], initialState: void 0, parse(context) { if (context.buffer.length < 1) return { success: false, consumed: 0, error: message`Expected an argument, but got end of input.` }; let i = 0; let optionsTerminated = context.optionsTerminated; if (!optionsTerminated) { if (context.buffer[i] === "--") { optionsTerminated = true; i++; } else if (context.buffer[i].match(optionPattern)) return { success: false, consumed: i, error: message`Expected an argument, but got an option: ${optionName(context.buffer[i])}.` }; } if (context.buffer.length < i + 1) return { success: false, consumed: i, error: message`Expected an argument, but got end of input.` }; if (context.state != null) return { success: false, consumed: i, error: message`The argument ${metavar(valueParser.metavar)} cannot be used multiple times.` }; const result = valueParser.parse(context.buffer[i]); return { success: true, next: { ...context, buffer: context.buffer.slice(i + 1), state: result, optionsTerminated }, consumed: context.buffer.slice(0, i + 1) }; }, complete(state) { if (state == null) return { success: false, error: message`Expected a ${metavar(valueParser.metavar)}, but too few arguments.` }; else if (state.success) return state; return { success: false, error: message`${metavar(valueParser.metavar)}: ${state.error}` }; }, getDocFragments(_state, defaultValue) { const fragments = [{ type: "entry", term, description: options.description, default: defaultValue == null ? void 0 : valueParser.format(defaultValue) }]; return { fragments, description: options.description }; }, [Symbol.for("Deno.customInspect")]() { return `argument()`; } }; } /** * Creates a parser that makes another parser optional, allowing it to succeed * without consuming input if the wrapped parser fails to match. * If the wrapped parser succeeds, this returns its value. * If the wrapped parser fails, this returns `undefined` without consuming input. * @template TValue The type of the value returned by the wrapped parser. * @template TState The type of the state used by the wrapped parser. * @param parser The {@link Parser} to make optional. * @returns A {@link Parser} that produces either the result of the wrapped parser * or `undefined` if the wrapped parser fails to match. */ function optional(parser) { return { $valueType: [], $stateType: [], priority: parser.priority, usage: [{ type: "optional", terms: parser.usage }], initialState: void 0, parse(context) { const result = parser.parse({ ...context, state: typeof context.state === "undefined" ? parser.initialState : context.state[0] }); if (result.success) return { success: true, next: { ...result.next, state: [result.next.state] }, consumed: result.consumed }; return result; }, complete(state) { if (typeof state === "undefined") return { success: true, value: void 0 }; return parser.complete(state[0]); }, getDocFragments(state, defaultValue) { const innerState = state.kind === "unavailable" ? { kind: "unavailable" } : state.state === void 0 ? { kind: "unavailable" } : { kind: "available", state: state.state[0] }; return parser.getDocFragments(innerState, defaultValue); } }; } /** * Creates a parser that makes another parser use a default value when it fails * to match or consume input. This is similar to {@link optional}, but instead * of returning `undefined` when the wrapped parser doesn't match, it returns * a specified default value. * @template TValue The type of the value returned by the wrapped parser. * @template TState The type of the state used by the wrapped parser. * @template TDefault The type of the default value. * @param parser The {@link Parser} to wrap with default behavior. * @param defaultValue The default value to return when the wrapped parser * doesn't match or consume input. Can be a value of type * {@link TDefault} or a function that returns such a value. * @returns A {@link Parser} that produces either the result of the wrapped parser * or the default value if the wrapped parser fails to match (union type {@link TValue} | {@link TDefault}). */ function withDefault(parser, defaultValue) { return { $valueType: [], $stateType: [], priority: parser.priority, usage: [{ type: "optional", terms: parser.usage }], initialState: void 0, parse(context) { const result = parser.parse({ ...context, state: typeof context.state === "undefined" ? parser.initialState : context.state[0] }); if (result.success) return { success: true, next: { ...result.next, state: [result.next.state] }, consumed: result.consumed }; return result; }, complete(state) { if (typeof state === "undefined") return { success: true, value: typeof defaultValue === "function" ? defaultValue() : defaultValue }; return parser.complete(state[0]); }, getDocFragments(state, upperDefaultValue) { const innerState = state.kind === "unavailable" ? { kind: "unavailable" } : state.state === void 0 ? { kind: "unavailable" } : { kind: "available", state: state.state[0] }; return parser.getDocFragments(innerState, upperDefaultValue != null ? upperDefaultValue : typeof defaultValue === "function" ? defaultValue() : defaultValue); } }; } /** * Creates a parser that transforms the result value of another parser using * a mapping function. This enables value transformation while preserving * the original parser's parsing logic and state management. * * The `map()` function is useful for: * - Converting parsed values to different types * - Applying transformations like string formatting or boolean inversion * - Computing derived values from parsed input * - Creating reusable transformations that can be applied to any parser * * @template T The type of the value produced by the original parser. * @template U The type of the value produced by the mapping function. * @template TState The type of the state used by the original parser. * @param parser The {@link Parser} whose result will be transformed. * @param transform A function that transforms the parsed value from type T to type U. * @returns A {@link Parser} that produces the transformed value of type U * while preserving the original parser's state type and parsing behavior. * * @example * ```typescript * // Transform boolean flag to its inverse * const parser = object({ * disallow: map(option("--allow"), b => !b) * }); * * // Transform string to uppercase * const upperParser = map(argument(string()), s => s.toUpperCase()); * * // Transform number to formatted string * const prefixedParser = map(option("-n", integer()), n => `value: ${n}`); * ``` */ function map(parser, transform) { return { $valueType: [], $stateType: parser.$stateType, priority: parser.priority, usage: parser.usage, initialState: parser.initialState, parse: parser.parse.bind(parser), complete(state) { const result = parser.complete(state); if (result.success) return { success: true, value: transform(result.value) }; return result; }, getDocFragments(state, _defaultValue) { return parser.getDocFragments(state, void 0); } }; } /** * Creates a parser that allows multiple occurrences of a given parser. * This parser can be used to parse multiple values of the same type, * such as multiple command-line arguments or options. * @template TValue The type of the value that the parser produces. * @template TState The type of the state used by the parser. * @param parser The {@link Parser} to apply multiple times. * @param options Optional configuration for the parser, * allowing you to specify the minimum and maximum number of * occurrences allowed. * @returns A {@link Parser} that produces an array of values * of type {@link TValue} and an array of states * of type {@link TState}. */ function multiple(parser, options = {}) { const { min = 0, max = Infinity } = options; return { $valueType: [], $stateType: [], priority: parser.priority, usage: [{ type: "multiple", terms: parser.usage, min }], initialState: [], parse(context) { let added = context.state.length < 1; let result = parser.parse({ ...context, state: context.state.at(-1) ?? parser.initialState }); if (!result.success) if (!added) { result = parser.parse({ ...context, state: parser.initialState }); if (!result.success) return result; added = true; } else return result; return { success: true, next: { ...result.next, state: [...added ? context.state : context.state.slice(0, -1), result.next.state] }, consumed: result.consumed }; }, complete(state) { const result = []; for (const s of state) { const valueResult = parser.complete(s); if (valueResult.success) result.push(valueResult.value); else return { success: false, error: valueResult.error }; } if (result.length < min) return { success: false, error: message`Expected at least ${text(min.toLocaleString("en"))} values, but got only ${text(result.length.toLocaleString("en"))}.` }; else if (result.length > max) return { success: false, error: message`Expected at most ${text(max.toLocaleString("en"))} values, but got ${text(result.length.toLocaleString("en"))}.` }; return { success: true, value: result }; }, getDocFragments(state, defaultValue) { const innerState = state.kind === "unavailable" ? { kind: "unavailable" } : state.state.length > 0 ? { kind: "available", state: state.state.at(-1) } : { kind: "unavailable" }; return parser.getDocFragments(innerState, defaultValue != null && defaultValue.length > 0 ? defaultValue[0] : void 0); } }; } function object(labelOrParsers, maybeParsers) { const label = typeof labelOrParsers === "string" ? labelOrParsers : void 0; const parsers = typeof labelOrParsers === "string" ? maybeParsers : labelOrParsers; const parserPairs = Object.entries(parsers); parserPairs.sort(([_, parserA], [__, parserB]) => parserB.priority - parserA.priority); return { $valueType: [], $stateType: [], priority: Math.max(...Object.values(parsers).map((p) => p.priority)), usage: parserPairs.flatMap(([_, p]) => p.usage), initialState: Object.fromEntries(Object.entries(parsers).map(([key, parser]) => [key, parser.initialState])), parse(context) { let error = { consumed: 0, error: context.buffer.length > 0 ? message`Unexpected option or argument: ${context.buffer[0]}.` : message`Expected an option or argument, but got end of input.` }; let currentContext = context; let anySuccess = false; const allConsumed = []; let madeProgress = true; while (madeProgress && currentContext.buffer.length > 0) { madeProgress = false; for (const [field, parser] of parserPairs) { const result = parser.parse({ ...currentContext, state: currentContext.state && typeof currentContext.state === "object" && field in currentContext.state ? currentContext.state[field] : parser.initialState }); if (result.success && result.consumed.length > 0) { currentContext = { ...currentContext, buffer: result.next.buffer, optionsTerminated: result.next.optionsTerminated, state: { ...currentContext.state, [field]: result.next.state } }; allConsumed.push(...result.consumed); anySuccess = true; madeProgress = true; break; } else if (!result.success && error.consumed < result.consumed) error = result; } } if (anySuccess) return { success: true, next: currentContext, consumed: allConsumed }; if (context.buffer.length === 0) { let allCanComplete = true; for (const [field, parser] of parserPairs) { const fieldState = context.state && typeof context.state === "object" && field in context.state ? context.state[field] : parser.initialState; const completeResult = parser.complete(fieldState); if (!completeResult.success) { allCanComplete = false; break; } } if (allCanComplete) return { success: true, next: context, consumed: [] }; } return { ...error, success: false }; }, complete(state) { const result = {}; for (const field in state) { if (!(field in parsers)) continue; const valueResult = parsers[field].complete(state[field]); if (valueResult.success) result[field] = valueResult.value; else return { success: false, error: valueResult.error }; } return { success: true, value: result }; }, getDocFragments(state, defaultValue) { const fragments = parserPairs.flatMap(([field, p]) => { const fieldState = state.kind === "unavailable" ? { kind: "unavailable" } : { kind: "available", state: state.state[field] }; return p.getDocFragments(fieldState, defaultValue?.[field]).fragments; }); const entries = fragments.filter((d) => d.type === "entry"); const sections = []; for (const fragment of fragments) { if (fragment.type !== "section") continue; if (fragment.title == null) entries.push(...fragment.entries); else sections.push(fragment); } const section = { title: label, entries }; sections.push(section); return { fragments: sections.map((s) => ({ ...s, type: "section" })) }; } }; } function tuple(labelOrParsers, maybeParsers) { const label = typeof labelOrParsers === "string" ? labelOrParsers : void 0; const parsers = typeof labelOrParsers === "string" ? maybeParsers : labelOrParsers; return { $valueType: [], $stateType: [], usage: parsers.toSorted((a, b) => b.priority - a.priority).flatMap((p) => p.usage), priority: parsers.length > 0 ? Math.max(...parsers.map((p) => p.priority)) : 0, initialState: parsers.map((parser) => parser.initialState), parse(context) { let currentContext = context; const allConsumed = []; const matchedParsers = /* @__PURE__ */ new Set(); while (matchedParsers.size < parsers.length) { let foundMatch = false; let error = { consumed: 0, error: message`No remaining parsers could match the input.` }; const remainingParsers = parsers.map((parser, index) => [parser, index]).filter(([_, index]) => !matchedParsers.has(index)).sort(([parserA], [parserB]) => parserB.priority - parserA.priority); for (const [parser, index] of remainingParsers) { const result = parser.parse({ ...currentContext, state: currentContext.state[index] }); if (result.success && result.consumed.length > 0) { currentContext = { ...currentContext, buffer: result.next.buffer, optionsTerminated: result.next.optionsTerminated, state: currentContext.state.map((s, idx) => idx === index ? result.next.state : s) }; allConsumed.push(...result.consumed); matchedParsers.add(index); foundMatch = true; break; } else if (!result.success && error.consumed < result.consumed) error = result; } if (!foundMatch) for (const [parser, index] of remainingParsers) { const result = parser.parse({ ...currentContext, state: currentContext.state[index] }); if (result.success && result.consumed.length < 1) { currentContext = { ...currentContext, state: currentContext.state.map((s, idx) => idx === index ? result.next.state : s) }; matchedParsers.add(index); foundMatch = true; break; } else if (!result.success && result.consumed < 1) { matchedParsers.add(index); foundMatch = true; break; } } if (!foundMatch) return { ...error, success: false }; } return { success: true, next: currentContext, consumed: allConsumed }; }, complete(state) { const result = []; for (let i = 0; i < parsers.length; i++) { const valueResult = parsers[i].complete(state[i]); if (valueResult.success) result[i] = valueResult.value; else return { success: false, error: valueResult.error }; } return { success: true, value: result }; }, getDocFragments(state, defaultValue) { const fragments = parsers.flatMap((p, i) => { const indexState = state.kind === "unavailable" ? { kind: "unavailable" } : { kind: "available", state: state.state[i] }; return p.getDocFragments(indexState, defaultValue?.[i]).fragments; }); const entries = fragments.filter((d) => d.type === "entry"); const sections = []; for (const fragment of fragments) { if (fragment.type !== "section") continue; if (fragment.title == null) entries.push(...fragment.entries); else sections.push(fragment); } const section = { title: label, entries }; sections.push(section); return { fragments: sections.map((s) => ({ ...s, type: "section" })) }; }, [Symbol.for("Deno.customInspect")]() { const parsersStr = parsers.length === 1 ? `[1 parser]` : `[${parsers.length} parsers]`; return label ? `tuple(${JSON.stringify(label)}, ${parsersStr})` : `tuple(${parsersStr})`; } }; } function or(...parsers) { return { $valueType: [], $stateType: [], priority: Math.max(...parsers.map((p) => p.priority)), usage: [{ type: "exclusive", terms: parsers.map((p) => p.usage) }], initialState: void 0, complete(state) { if (state == null) return { success: false, error: message`No parser matched.` }; const [i, result] = state; if (result.success) return parsers[i].complete(result.next.state); return { success: false, error: result.error }; }, parse(context) { let error = { consumed: 0, error: context.buffer.length < 1 ? message`No parser matched.` : message`Unexpected option or subcommand: ${optionName(context.buffer[0])}.` }; const orderedParsers = parsers.map((p, i) => [p, i]); orderedParsers.sort(([_, a], [__, b]) => context.state?.[0] === a ? -1 : context.state?.[0] === b ? 1 : a - b); for (const [parser, i] of orderedParsers) { const result = parser.parse({ ...context, state: context.state == null || context.state[0] !== i || !context.state[1].success ? parser.initialState : context.state[1].next.state }); if (result.success && result.consumed.length > 0) { if (context.state?.[0] !== i && context.state?.[1].success) return { success: false, consumed: context.buffer.length - result.next.buffer.length, error: message`${values(context.state[1].consumed)} and ${values(result.consumed)} cannot be used together.` }; return { success: true, next: { ...context, buffer: result.next.buffer, optionsTerminated: result.next.optionsTerminated, state: [i, result] }, consumed: result.consumed }; } else if (!result.success && error.consumed < result.consumed) error = result; } return { ...error, success: false }; }, getDocFragments(state, _defaultValue) { let description; let fragments; if (state.kind === "unavailable" || state.state == null) fragments = parsers.flatMap((p) => p.getDocFragments({ kind: "unavailable" }, void 0).fragments); else { const [index, parserResult] = state.state; const innerState = parserResult.success ? { kind: "available", state: parserResult.next.state } : { kind: "unavailable" }; const docFragments = parsers[index].getDocFragments(innerState, void 0); description = docFragments.description; fragments = docFragments.fragments; } const entries = fragments.filter((f) => f.type === "entry"); const sections = []; for (const fragment of fragments) { if (fragment.type !== "section") continue; if (fragment.title == null) entries.push(...fragment.entries); else sections.push(fragment); } return { description, fragments: [...sections.map((s) => ({ ...s, type: "section" })), { type: "section", entries }] }; } }; } function longestMatch(...parsers) { return { $valueType: [], $stateType: [], priority: Math.max(...parsers.map((p) => p.priority)), usage: [{ type: "exclusive", terms: parsers.map((p) => p.usage) }], initialState: void 0, complete(state) { if (state == null) return { success: false, error: message`No parser matched.` }; const [i, result] = state; if (result.success) return parsers[i].complete(result.next.state); return { success: false, error: result.error }; }, parse(context) { let bestMatch = null; let error = { consumed: 0, error: context.buffer.length < 1 ? message`No parser matched.` : message`Unexpected option or subcommand: ${optionName(context.buffer[0])}.` }; for (let i = 0; i < parsers.length; i++) { const parser = parsers[i]; const result = parser.parse({ ...context, state: context.state == null || context.state[0] !== i || !context.state[1].success ? parser.initialState : context.state[1].next.state }); if (result.success) { const consumed = context.buffer.length - result.next.buffer.length; if (bestMatch === null || consumed > bestMatch.consumed) bestMatch = { index: i, result, consumed }; } else if (error.consumed < result.consumed) error = result; } if (bestMatch && bestMatch.result.success) return { success: true, next: { ...context, buffer: bestMatch.result.next.buffer, optionsTerminated: bestMatch.result.next.optionsTerminated, state: [bestMatch.index, bestMatch.result] }, consumed: bestMatch.result.consumed }; return { ...error, success: false }; }, getDocFragments(state, _defaultValue) { let description; let fragments; if (state.kind === "unavailable" || state.state == null) fragments = parsers.flatMap((p) => p.getDocFragments({ kind: "unavailable" }).fragments); else { const [i, result] = state.state; if (result.success) { const docResult = parsers[i].getDocFragments({ kind: "available", state: result.next.state }); description = docResult.description; fragments = docResult.fragments; } else fragments = parsers.flatMap((p) => p.getDocFragments({ kind: "unavailable" }).fragments); } return { description, fragments }; } }; } function merge(...args) { const label = typeof args[0] === "string" ? args[0] : void 0; let parsers = typeof args[0] === "string" ? args.slice(1) : args; parsers = parsers.toSorted((a, b) => b.priority - a.priority); const initialState = {}; for (const parser of parsers) if (parser.initialState && typeof parser.initialState === "object") for (const field in parser.initialState) initialState[field] = parser.initialState[field]; return { $valueType: [], $stateType: [], priority: Math.max(...parsers.map((p) => p.priority)), usage: parsers.flatMap((p) => p.usage), initialState, parse(context) { for (let i = 0; i < parsers.length; i++) { const parser = parsers[i]; let parserState; if (parser.initialState === void 0) parserState = void 0; else if (parser.initialState && typeof parser.initialState === "object") if (context.state && typeof context.state === "object") { const extractedState = {}; for (const field in parser.initialState) extractedState[field] = field in context.state ? context.state[field] : parser.initialState[field]; parserState = extractedState; } else parserState = parser.initialState; else parserState = parser.initialState; const result = parser.parse({ ...context, state: parserState }); if (result.success) { let newState; if (parser.initialState === void 0) newState = { ...context.state, [`__parser_${i}`]: result.next.state }; else newState = { ...context.state, ...result.next.state }; return { success: true, next: { ...context, buffer: result.next.buffer, optionsTerminated: result.next.optionsTerminated, state: newState }, consumed: result.consumed }; } else if (result.consumed < 1) continue; else return result; } return { success: false, consumed: 0, error: message`No parser matched the input.` }; }, complete(state) { const object$1 = {}; for (let i = 0; i < parsers.length; i++) { const parser = parsers[i]; let parserState; if (parser.initialState === void 0) { const key = `__parser_${i}`; if (state && typeof state === "object" && key in state) parserState = state[key]; else parserState = void 0; } else if (parser.initialState && typeof parser.initialState === "object") if (state && typeof state === "object") { const extractedState = {}; for (const field in parser.initialState) extractedState[field] = field in state ? state[field] : parser.initialState[field]; parserState = extractedState; } else parserState = parser.initialState; else parserState = parser.initialState; const result = parser.complete(parserState); if (!result.success) return result; for (const field in result.value) object$1[field] = result.value[field]; } return { success: true, value: object$1 }; }, getDocFragments(state, _defaultValue) { const fragments = parsers.flatMap((p) => { const parserState = p.initialState === void 0 ? { kind: "unavailable" } : state.kind === "unavailable" ? { kind: "unavailable" } : { kind: "available", state: state.state }; return p.getDocFragments(parserState, void 0).fragments; }); const entries = fragments.filter((f) => f.type === "entry"); const sections = []; for (const fragment of fragments) { if (fragment.type !== "section") continue; if (fragment.title == null) entries.push(...fragment.entries); else sections.push(fragment); } if (label) { const labeledSection = { title: label, entries }; sections.push(labeledSection); return { fragments: sections.map((s) => ({ ...s, type: "section" })) }; } return { fragments: [...sections.map((s) => ({ ...s, type: "section" })), { type: "section", entries }] }; } }; } function concat(...parsers) { const initialState = parsers.map((parser) => parser.initialState); return { $valueType: [], $stateType: [], priority: parsers.length > 0 ? Math.max(...parsers.map((p) => p.priority)) : 0, usage: parsers.flatMap((p) => p.usage), initialState, parse(context) { let currentContext = context; const allConsumed = []; const matchedParsers = /* @__PURE__ */ new Set(); while (matchedParsers.size < parsers.length) { let foundMatch = false; let error = { consumed: 0, error: message`No remaining parsers could match the input.` }; const remainingParsers = parsers.map((parser, index) => [parser, index]).filter(([_, index]) => !matchedParsers.has(index)).sort(([parserA], [parserB]) => parserB.priority - parserA.priority); for (const [parser, index] of remainingParsers) { const result = parser.parse({ ...currentContext, state: currentContext.state[index] }); if (result.success && result.consumed.length > 0) { currentContext = { ...currentContext, buffer: result.next.buffer, optionsTerminated: result.next.optionsTerminated, state: currentContext.state.map((s, idx) => idx === index ? result.next.state : s) }; allConsumed.push(...result.consumed); matchedParsers.add(index); foundMatch = true; break; } else if (!result.success && error.consumed < result.consumed) error = result; } if (!foundMatch) for (const [parser, index] of remainingParsers) { const result = parser.parse({ ...currentContext, state: currentContext.state[index] }); if (result.success && result.consumed.length < 1) { currentContext = { ...currentContext, state: currentContext.state.map((s, idx) => idx === index ? result.next.state : s) }; matchedParsers.add(index); foundMatch = true; break; } else if (!result.success && result.consumed < 1) { matchedParsers.add(index); foundMatch = true; break; } } if (!foundMatch) return { ...error, success: false }; } return { success: true, next: currentContext, consumed: allConsumed }; }, complete(state) { const results = []; for (let i = 0; i < parsers.length; i++) { const parser = parsers[i]; const parserState = state[i]; const result = parser.complete(parserState); if (!result.success) return result; if (Array.isArray(result.value)) results.push(...result.value); else results.push(result.value); } return { success: true, value: results }; }, getDocFragments(state, _defaultValue) { const fragments = parsers.flatMap((p, index) => { const indexState = state.kind === "unavailable" ? { kind: "unavailable" } : { kind: "available", state: state.state[index] }; return p.getDocFragments(indexState, void 0).fragments; }); const entries = fragments.filter((f) => f.type === "entry"); const sections = []; for (const fragment of fragments) { if (fragment.type !== "section") continue; if (fragment.title == null) entries.push(...fragment.entries); else sections.push(fragment); } const result = [...sections.map((s) => ({ ...s, type: "section" }))]; if (entries.length > 0) result.push({ type: "section", entries }); return { fragments: result }; } }; } /** * Creates a parser that matches a specific subcommand name and then applies * an inner parser to the remaining arguments. * This is useful for building CLI tools with subcommands like git, npm, etc. * @template T The type of the value returned by the inner parser. * @template TState The type of the state used by the inner parser. * @param name The subcommand name to match (e.g., `"show"`, `"edit"`). * @param parser The {@link Parser} to apply after the command is matched. * @param options Optional configuration for the command parser, such as * a description for documentation. * @returns A {@link Parser} that matches the command name and delegates * to the inner parser for the remaining arguments. */ function command(name, parser, options = {}) { return { $valueType: [], $stateType: [], priority: 15, usage: [{ type: "command", name }, ...parser.usage], initialState: void 0, parse(context) { if (context.state === void 0) { if (context.buffer.length < 1 || context.buffer[0] !== name) return { success: false, consumed: 0, error: message`Expected command ${optionName(name)}, but got ${context.buffer.length > 0 ? context.buffer[0] : "end of input"}.` }; return { success: true, next: { ...context, buffer: context.buffer.slice(1), state: ["matched", name] }, consumed: context.buffer.slice(0, 1) }; } else if (context.state[0] === "matched") { const result = parser.parse({ ...context, state: parser.initialState }); if (result.success) return { success: true, next: { ...result.next, state: ["parsing", result.next.state] }, consumed: result.consumed }; return result; } else if (context.state[0] === "parsing") { const result = parser.parse({ ...context, state: context.state[1] }); if (result.success) return { success: true, next: { ...result.next, state: ["parsing", result.next.state] }, consumed: result.consumed }; return result; } return { success: false, consumed: 0, error: message`Invalid command state.` }; }, complete(state) { if (typeof state === "undefined") return { success: false, error: message`Command ${optionName(name)} was not matched.` }; else if (state[0] === "matched") return parser.complete(parser.initialState); else if (state[0] === "parsing") return parser.complete(state[1]); return { success: false, error: message`Invalid command state during completion.` }; }, getDocFragments(state, defaultValue) { if (state.kind === "unavailable" || typeof state.state === "undefined") return { description: options.description, fragments: [{ type: "entry", term: { type: "command", name }, description: options.description }] }; const innerState = state.state[0] === "parsing" ? { kind: "available", state: state.state[1] } : { kind: "unavailable" }; const innerFragments = parser.getDocFragments(innerState, defaultValue); return { ...innerFragments, description: innerFragments.description ?? options.description }; }, [Symbol.for("Deno.customInspect")]() { return `command(${JSON.stringify(name)})`; } }; } /** * Parses an array of command-line arguments using the provided combined parser. * This function processes the input arguments, applying the parser to each * argument until all arguments are consumed or an error occurs. * @template T The type of the value produced by the parser. * @param parser The combined {@link Parser} to use for parsing the input * arguments. * @param args The array of command-line arguments to parse. Usually this is * `process.argv.slice(2)` in Node.js or `Deno.args` in Deno. * @returns A {@link Result} object indicating whether the parsing was * successful or not. If successful, it contains the parsed value of * type `T`. If not, it contains an error message describing the * failure. */ function parse(parser, args) { let context = { buffer: args, optionsTerminated: false, state: parser.initialState }; do { const result = parser.parse(context); if (!result.success) return { success: false, error: result.error }; const previousBuffer = context.buffer; context = result.next; if (context.buffer.length > 0 && context.buffer.length === previousBuffer.length && context.buffer[0] === previousBuffer[0]) return { success: false, error: message`Unexpected option or argument: ${context.buffer[0]}.` }; } while (context.buffer.length > 0); const endResult = parser.complete(context.state); return endResult.success ? { success: true, value: endResult.value } : { success: false, error: endResult.error }; } /** * Wraps a parser with a group label for documentation purposes. * * The `group()` function is a documentation-only wrapper that applies a label * to any parser for help text organization. This allows you to use clean code * structure with combinators like {@link merge} while maintaining well-organized * help text through group labeling. * * The wrapped parser has identical parsing behavior but generates documentation * fragments wrapped in a labeled section. This is particularly useful when * combining parsers using {@link merge}—you can wrap the merged result with * `group()` to add a section header in help output. * * @example * ```typescript * const apiOptions = merge( * object({ endpoint: option("--endpoint", string()) }), * object({ timeout: option("--timeout", integer()) }) * ); * * const groupedApiOptions = group("API Options", apiOptions); * // Now produces a labeled "API Options" section in help text * ``` * * @example * ```typescript * // Can be used with any parser, not just merge() * const verboseGroup = group("Verbosity", object({ * verbose: option("-v", "--verbose"), * quiet: option("-q", "--quiet") * })); * ``` * * @template TValue The value type of the wrapped parser. * @template TState The state type of the wrapped parser. * @param label A descriptive label for this parser group, used for * documentation and help text organization. * @param parser The parser to wrap with a group label. * @returns A new parser that behaves identically to the input parser * but generates documentation within a labeled section. * @since 0.4.0 */ function group(label, parser) { return { $valueType: parser.$valueType, $stateType: parser.$stateType, priority: parser.priority, usage: parser.usage, initialState: parser.initialState, parse: (context) => parser.parse(context), complete: (state) => parser.complete(state), getDocFragments: (state, defaultValue) => { const { description, fragments } = parser.getDocFragments(state, defaultValue); const allEntries = []; const titledSections = []; for (const fragment of fragments) if (fragment.type === "entry") allEntries.push(fragment); else if (fragment.type === "section") if (fragment.title) titledSections.push(fragment); else allEntries.push(...fragment.entries); const labeledSection = { title: label, entries: allEntries }; return { description, fragments: [...titledSections.map((s) => ({ ...s, type: "section" })), { type: "section", ...labeledSection }] }; } }; } /** * Generates a documentation page for a parser based on its current state after * attempting to parse the provided arguments. This function is useful for * creating help documentation that reflects the current parsing context. * * The function works by: * 1. Attempting to parse the provided arguments to determine the current state * 2. Generating documentation fragments from the parser's current state * 3. Organizing fragments into entries and sections * 4. Resolving command usage terms based on parsed arguments * * @param parser The parser to generate documentation for * @param args Optional array of command-line arguments that have been parsed * so far. Defaults to an empty array. This is used to determine * the current parsing context and generate contextual documentation. * @returns A {@link DocPage} containing usage information, sections, and * optional description, or `undefined` if no documentation can be *