@optique/core
Version:
Type-safe combinatorial command-line interface parser
1,586 lines (1,585 loc) • 51.7 kB
JavaScript
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
*