| /** | |
| * Parsing Pipeline for CLI Commands | |
| * ================================== | |
| * | |
| * The parser transforms raw argv tokens into structured command input through | |
| * three main phases: | |
| * | |
| * 1. **Lexer** (external): Converts argv strings into typed tokens | |
| * - LongOption: --name or --name=value | |
| * - ShortOption: -n or -n=value (also handles -abc as three flags) | |
| * - Value: positional arguments | |
| * | |
| * 2. **Built-in Extraction**: Peels off built-in flags (help/version/completions) | |
| * before command-specific parsing begins. | |
| * | |
| * 3. **Command Parsing**: Recursively processes command levels: | |
| * - Collects flags defined at this level | |
| * - Detects subcommand from first value token | |
| * - Forwards remaining tokens to child command | |
| * | |
| * Key Behaviors | |
| * ------------- | |
| * - Inherited parent flags may appear before OR after the subcommand name (npm-style) | |
| * - Only the first Value token can open a subcommand | |
| * - Errors accumulate rather than throwing exceptions | |
| */ | |
| import * as Effect from "../../../Effect.js"; | |
| import * as Option from "../../../Option.js"; | |
| import * as CliError from "../CliError.js"; | |
| import * as Param from "../Param.js"; | |
| import * as Primitive from "../Primitive.js"; | |
| import { suggest } from "./auto-suggest.js"; | |
| import { toImpl } from "./command.js"; | |
| /* ========================================================================== */ | |
| /* Public API */ | |
| /* ========================================================================== */ | |
| /** @internal */ | |
| export const getCommandPath = parsedInput => Option.match(parsedInput.subcommand, { | |
| onNone: () => [], | |
| onSome: subcommand => [subcommand.name, ...getCommandPath(subcommand.parsedInput)] | |
| }); | |
| /** @internal */ | |
| export const parseArgs = (lexResult, command, commandPath = []) => Effect.gen(function* () { | |
| const { | |
| tokens, | |
| trailingOperands: afterEndOfOptions | |
| } = lexResult; | |
| const newCommandPath = [...commandPath, command.name]; | |
| const commandImpl = toImpl(command); | |
| const singles = commandImpl.config.flags.flatMap(Param.extractSingleParams); | |
| const flagParams = singles.filter(Param.isFlagParam); | |
| const flagRegistry = createFlagRegistry(flagParams); | |
| const inheritedSingles = commandImpl.contextConfig.flags.flatMap(Param.extractSingleParams); | |
| const inheritedFlagParams = inheritedSingles.filter(Param.isFlagParam); | |
| const inheritedFlagRegistry = createFlagRegistry(inheritedFlagParams); | |
| const inheritedNames = new Set(inheritedFlagParams.map(param => param.name)); | |
| const context = { | |
| command, | |
| commandPath: newCommandPath, | |
| flagRegistry, | |
| inheritedFlagRegistry, | |
| localFlagNames: flagParams.filter(param => !inheritedNames.has(param.name)).map(param => param.name) | |
| }; | |
| const result = scanCommandLevel(tokens, context); | |
| if (result._tag === "Leaf") { | |
| return { | |
| flags: result.flags, | |
| arguments: [...result.arguments, ...afterEndOfOptions], | |
| subcommand: Option.none(), | |
| ...(result.errors.length > 0 && { | |
| errors: result.errors | |
| }) | |
| }; | |
| } | |
| const subLex = { | |
| tokens: result.childTokens, | |
| trailingOperands: [] | |
| }; | |
| const subParsed = yield* parseArgs(subLex, result.sub, newCommandPath); | |
| const allErrors = [...result.errors, ...(subParsed.errors ?? [])]; | |
| return { | |
| flags: result.flags, | |
| arguments: afterEndOfOptions, | |
| subcommand: Option.some({ | |
| name: result.sub.name, | |
| parsedInput: subParsed | |
| }), | |
| ...(allErrors.length > 0 && { | |
| errors: allErrors | |
| }) | |
| }; | |
| }); | |
| const makeCursor = tokens => { | |
| let i = 0; | |
| return { | |
| peek: () => tokens[i], | |
| take: () => tokens[i++], | |
| rest: () => tokens.slice(i) | |
| }; | |
| }; | |
| /* ========================================================================== */ | |
| /* Flag Registry */ | |
| /* ========================================================================== */ | |
| /** | |
| * Creates a registry for O(1) flag lookup by name or alias. | |
| * @internal | |
| */ | |
| export const createFlagRegistry = params => { | |
| const index = new Map(); | |
| for (const param of params) { | |
| if (index.has(param.name)) { | |
| throw new Error(`Duplicate flag name "${param.name}" in command definition`); | |
| } | |
| index.set(param.name, param); | |
| for (const alias of param.aliases) { | |
| if (index.has(alias)) { | |
| throw new Error(`Duplicate flag/alias "${alias}" in command definition (conflicts with "${index.get(alias).name}")`); | |
| } | |
| index.set(alias, param); | |
| } | |
| } | |
| return { | |
| params, | |
| index | |
| }; | |
| }; | |
| const buildSubcommandIndex = subcommands => { | |
| const index = new Map(); | |
| const setKey = (key, command) => { | |
| const existing = index.get(key); | |
| if (existing && existing !== command) { | |
| throw new Error(`Duplicate subcommand name/alias "${key}" in command definition (conflicts with "${existing.name}")`); | |
| } | |
| index.set(key, command); | |
| }; | |
| for (const group of subcommands) { | |
| for (const subcommand of group.commands) { | |
| setKey(subcommand.name, subcommand); | |
| if (subcommand.alias && subcommand.alias !== subcommand.name) { | |
| setKey(subcommand.alias, subcommand); | |
| } | |
| } | |
| } | |
| return index; | |
| }; | |
| /* ========================================================================== */ | |
| /* Flag Accumulator */ | |
| /* ========================================================================== */ | |
| /** Creates an empty flag map with all known flag names initialized to []. | |
| * @internal | |
| */ | |
| export const createEmptyFlagMap = params => Object.fromEntries(params.map(p => [p.name, []])); | |
| /** | |
| * Creates a mutable accumulator for collecting flag values. | |
| * Pre-initializes empty arrays for all known flags. | |
| */ | |
| const createFlagAccumulator = params => { | |
| const map = createEmptyFlagMap(params); | |
| return { | |
| add: (name, raw) => { | |
| if (raw !== undefined) map[name].push(raw); | |
| }, | |
| merge: from => { | |
| for (const key in from) { | |
| const values = from[key]; | |
| if (values?.length) { | |
| for (let i = 0; i < values.length; i++) { | |
| map[key].push(values[i]); | |
| } | |
| } | |
| } | |
| }, | |
| snapshot: () => map | |
| }; | |
| }; | |
| const isFlagToken = t => t._tag === "LongOption" || t._tag === "ShortOption"; | |
| const getFlagName = t => t._tag === "LongOption" ? t.name : t.flag; | |
| const resolveFlag = (token, registry) => { | |
| const tokenName = getFlagName(token); | |
| const direct = registry.index.get(tokenName); | |
| if (direct && direct.name === tokenName) { | |
| return { | |
| param: direct, | |
| negated: false | |
| }; | |
| } | |
| if (token._tag === "LongOption" && token.name.startsWith("no-")) { | |
| const canonicalName = token.name.slice(3); | |
| const param = registry.index.get(canonicalName); | |
| if (param && param.name === canonicalName && Primitive.isBoolean(param.primitiveType)) { | |
| return { | |
| param, | |
| negated: true | |
| }; | |
| } | |
| } | |
| if (direct) { | |
| return { | |
| param: direct, | |
| negated: false | |
| }; | |
| } | |
| return undefined; | |
| }; | |
| const invalidNegatedFlagValue = (token, spec, value) => new CliError.InvalidValue({ | |
| option: spec.name, | |
| value, | |
| expected: `omit the value and use ${token.raw} by itself to set --${spec.name} to false`, | |
| kind: "flag" | |
| }); | |
| /** | |
| * Checks whether a token is a boolean literal value. | |
| * Recognizes: true/false, yes/no, on/off, 1/0 | |
| */ | |
| const asBooleanLiteral = token => token?._tag === "Value" && (Primitive.isTrueValue(token.value) || Primitive.isFalseValue(token.value)) ? token.value : undefined; | |
| /* ========================================================================== */ | |
| /* Flag Value Consumption */ | |
| /* ========================================================================== */ | |
| /** | |
| * Reads a flag's value from the token stream. | |
| * | |
| * Value resolution order: | |
| * 1. Inline value: --flag=value or -f=value | |
| * 2. Boolean special case: implicit "true" or explicit boolean literal | |
| * 3. Next token: consume following Value token if present | |
| */ | |
| const consumeFlagValue = (cursor, token, spec, negated = false) => { | |
| const consumed = consumeFlagValueWithTokens(cursor, token, spec, negated); | |
| switch (consumed._tag) { | |
| case "Value": | |
| return { | |
| _tag: "Value", | |
| value: consumed.value | |
| }; | |
| case "Error": | |
| return { | |
| _tag: "Error", | |
| error: consumed.error | |
| }; | |
| } | |
| }; | |
| const consumeFlagValueWithTokens = (cursor, token, spec, negated = false) => { | |
| // Inline value has highest priority | |
| if (negated) { | |
| if (token.value !== undefined) { | |
| return { | |
| _tag: "Error", | |
| error: invalidNegatedFlagValue(token, spec, token.value), | |
| tokens: [] | |
| }; | |
| } | |
| const literal = asBooleanLiteral(cursor.peek()); | |
| if (literal !== undefined) { | |
| const literalToken = cursor.take(); | |
| return { | |
| _tag: "Error", | |
| error: invalidNegatedFlagValue(token, spec, literal), | |
| tokens: literalToken === undefined ? [] : [literalToken] | |
| }; | |
| } | |
| return { | |
| _tag: "Value", | |
| value: "false", | |
| tokens: [] | |
| }; | |
| } | |
| if (token.value !== undefined) { | |
| return { | |
| _tag: "Value", | |
| value: token.value, | |
| tokens: [] | |
| }; | |
| } | |
| // Boolean flags: check for explicit literal or default to "true" | |
| if (Primitive.isBoolean(spec.primitiveType)) { | |
| const literal = asBooleanLiteral(cursor.peek()); | |
| const literalToken = literal !== undefined ? cursor.take() : undefined; | |
| return { | |
| _tag: "Value", | |
| value: literal ?? "true", | |
| tokens: literalToken === undefined ? [] : [literalToken] | |
| }; | |
| } | |
| // Non-boolean: try to consume next Value token | |
| const next = cursor.peek(); | |
| if (next?._tag === "Value") { | |
| const valueToken = cursor.take(); | |
| return { | |
| _tag: "Value", | |
| value: next.value, | |
| tokens: valueToken === undefined ? [] : [valueToken] | |
| }; | |
| } | |
| return { | |
| _tag: "Value", | |
| value: undefined, | |
| tokens: [] | |
| }; | |
| }; | |
| /** | |
| * Consumes known flags from a token stream. | |
| * Unrecognized tokens are passed through to remainder. | |
| * Used for both global flag extraction and npm-style parent flag collection. | |
| * @internal | |
| */ | |
| export const consumeKnownFlags = (tokens, registry) => { | |
| const flagMap = createEmptyFlagMap(registry.params); | |
| const remainder = []; | |
| const errors = []; | |
| const cursor = makeCursor(tokens); | |
| for (let t = cursor.take(); t; t = cursor.take()) { | |
| if (!isFlagToken(t)) { | |
| remainder.push(t); | |
| continue; | |
| } | |
| const resolved = resolveFlag(t, registry); | |
| if (!resolved) { | |
| remainder.push(t); | |
| continue; | |
| } | |
| const consumed = consumeFlagValue(cursor, t, resolved.param, resolved.negated); | |
| if (consumed._tag === "Error") { | |
| errors.push(consumed.error); | |
| continue; | |
| } | |
| if (consumed.value !== undefined) { | |
| flagMap[resolved.param.name].push(consumed.value); | |
| } | |
| } | |
| return { | |
| flagMap, | |
| remainder, | |
| errors | |
| }; | |
| }; | |
| const extractFlagParams = command => { | |
| const commandImpl = toImpl(command); | |
| const singles = commandImpl.config.flags.flatMap(Param.extractSingleParams); | |
| return singles.filter(Param.isFlagParam); | |
| }; | |
| const extractContextFlagParams = command => { | |
| const commandImpl = toImpl(command); | |
| const singles = commandImpl.contextConfig.flags.flatMap(Param.extractSingleParams); | |
| return singles.filter(Param.isFlagParam); | |
| }; | |
| const resolveFromRegistries = (token, registries) => { | |
| for (const registry of registries) { | |
| const resolved = resolveFlag(token, registry); | |
| if (resolved !== undefined) { | |
| return resolved; | |
| } | |
| } | |
| return undefined; | |
| }; | |
| const preserveFlag = (remainder, cursor, token, resolved) => { | |
| remainder.push(token); | |
| const consumed = consumeFlagValueWithTokens(cursor, token, resolved.param, resolved.negated); | |
| remainder.push(...consumed.tokens); | |
| }; | |
| const localFlagWouldPrecedeSubcommand = (token, remainingTokens, resolved, subIndex, registries) => { | |
| const cursor = makeCursor(remainingTokens); | |
| consumeFlagValueWithTokens(cursor, token, resolved.param, resolved.negated); | |
| for (let token = cursor.take(); token; token = cursor.take()) { | |
| if (isFlagToken(token)) { | |
| const known = resolveFromRegistries(token, registries); | |
| if (known !== undefined) { | |
| consumeFlagValueWithTokens(cursor, token, known.param, known.negated); | |
| } | |
| continue; | |
| } | |
| if (token._tag === "Value") { | |
| return subIndex.has(token.value); | |
| } | |
| } | |
| return false; | |
| }; | |
| /** | |
| * Consumes global flags while walking the command tree. | |
| * | |
| * Command-local flags take precedence over global flags at the selected command | |
| * level. This lets commands reuse a global flag name, for example a subcommand | |
| * with its own `--version <value>` flag overriding the built-in global | |
| * `--version` action. | |
| * @internal | |
| */ | |
| export const consumeGlobalFlags = (tokens, command, registry) => { | |
| const flagMap = createEmptyFlagMap(registry.params); | |
| const errors = []; | |
| const consumeLevel = (tokens, command, ignoredRegistries) => { | |
| const localRegistry = createFlagRegistry(extractFlagParams(command)); | |
| const inheritedRegistry = createFlagRegistry(extractContextFlagParams(command)); | |
| const subIndex = buildSubcommandIndex(command.subcommands); | |
| const cursor = makeCursor(tokens); | |
| const remainder = []; | |
| let awaitingFirstValue = true; | |
| for (let token = cursor.take(); token; token = cursor.take()) { | |
| if (isFlagToken(token)) { | |
| const ignored = resolveFromRegistries(token, ignoredRegistries); | |
| if (ignored !== undefined) { | |
| preserveFlag(remainder, cursor, token, ignored); | |
| continue; | |
| } | |
| const inherited = resolveFlag(token, inheritedRegistry); | |
| if (inherited !== undefined) { | |
| preserveFlag(remainder, cursor, token, inherited); | |
| continue; | |
| } | |
| const local = resolveFlag(token, localRegistry); | |
| const global = resolveFlag(token, registry); | |
| if (local !== undefined) { | |
| if (global === undefined || !awaitingFirstValue || !localFlagWouldPrecedeSubcommand(token, cursor.rest(), local, subIndex, [localRegistry, inheritedRegistry, registry])) { | |
| preserveFlag(remainder, cursor, token, local); | |
| continue; | |
| } | |
| } | |
| if (global !== undefined) { | |
| const consumed = consumeFlagValueWithTokens(cursor, token, global.param, global.negated); | |
| if (consumed._tag === "Error") { | |
| errors.push(consumed.error); | |
| continue; | |
| } | |
| if (consumed.value !== undefined) { | |
| flagMap[global.param.name].push(consumed.value); | |
| } | |
| continue; | |
| } | |
| remainder.push(token); | |
| continue; | |
| } | |
| if (token._tag === "Value" && awaitingFirstValue) { | |
| const sub = subIndex.get(token.value); | |
| if (sub !== undefined) { | |
| remainder.push(token); | |
| remainder.push(...consumeLevel(cursor.rest(), sub, [...ignoredRegistries, inheritedRegistry])); | |
| return remainder; | |
| } | |
| awaitingFirstValue = false; | |
| } | |
| remainder.push(token); | |
| } | |
| return remainder; | |
| }; | |
| return { | |
| flagMap, | |
| remainder: consumeLevel(tokens, command, []), | |
| errors | |
| }; | |
| }; | |
| /* ========================================================================== */ | |
| /* Error Creation */ | |
| /* ========================================================================== */ | |
| const createUnrecognizedFlagError = (token, params, commandPath) => { | |
| const printable = token._tag === "LongOption" ? `--${token.name}` : `-${token.flag}`; | |
| const validNames = []; | |
| for (const p of params) { | |
| // Exclude hidden flags so a near-miss typo cannot reveal a flag name | |
| // that was intentionally kept out of --help. | |
| if (p.hidden) continue; | |
| validNames.push(p.name); | |
| if (Primitive.isBoolean(p.primitiveType)) { | |
| validNames.push(`no-${p.name}`); | |
| } | |
| for (const alias of p.aliases) { | |
| validNames.push(alias); | |
| } | |
| } | |
| const suggestions = suggest(getFlagName(token), validNames).map(n => n.length === 1 ? `-${n}` : `--${n}`); | |
| return new CliError.UnrecognizedOption({ | |
| option: printable, | |
| suggestions, | |
| command: commandPath | |
| }); | |
| }; | |
| /* ========================================================================== */ | |
| /* Parse State */ | |
| /* ========================================================================== */ | |
| const createParseState = registry => ({ | |
| flags: createFlagAccumulator(registry.params), | |
| arguments: [], | |
| errors: [], | |
| mode: { | |
| _tag: "AwaitingFirstValue" | |
| } | |
| }); | |
| const toLeafResult = state => ({ | |
| _tag: "Leaf", | |
| flags: state.flags.snapshot(), | |
| arguments: state.arguments, | |
| errors: state.errors | |
| }); | |
| /* ========================================================================== */ | |
| /* First Value Resolution */ | |
| /* ========================================================================== */ | |
| /** | |
| * Determines how to handle the first value token. | |
| * | |
| * If it matches a known subcommand: | |
| * - Collect inherited parent flags from remaining tokens (npm-style) | |
| * - Return SubcommandResult with child tokens | |
| * | |
| * Otherwise: | |
| * - Return Argument to treat it as a positional argument | |
| * - Report error if command expects subcommand but got unknown value | |
| */ | |
| const resolveFirstValue = (value, cursor, context, state) => { | |
| const { | |
| command, | |
| commandPath, | |
| inheritedFlagRegistry, | |
| localFlagNames | |
| } = context; | |
| const subIndex = buildSubcommandIndex(command.subcommands); | |
| const sub = subIndex.get(value); | |
| if (sub) { | |
| const selectedPath = [...commandPath, sub.name]; | |
| // Local flags are not inherited by subcommands. | |
| const parentFlags = state.flags.snapshot(); | |
| for (const localFlagName of localFlagNames) { | |
| const values = parentFlags[localFlagName]; | |
| if (values !== undefined && values.length > 0) { | |
| state.errors.push(new CliError.UnrecognizedOption({ | |
| option: `--${localFlagName}`, | |
| suggestions: [], | |
| command: selectedPath | |
| })); | |
| } | |
| } | |
| // npm-style: inherited parent flags can appear after subcommand name | |
| const tail = consumeKnownFlags(cursor.rest(), inheritedFlagRegistry); | |
| state.flags.merge(tail.flagMap); | |
| state.errors.push(...tail.errors); | |
| return { | |
| _tag: "Subcommand", | |
| result: { | |
| _tag: "Sub", | |
| flags: state.flags.snapshot(), | |
| sub, | |
| childTokens: tail.remainder, | |
| errors: state.errors | |
| } | |
| }; | |
| } | |
| // Not a subcommand. Check if this looks like a typo. | |
| const expectsArgs = toImpl(command).config.arguments.length > 0; | |
| if (!expectsArgs && subIndex.size > 0) { | |
| // Exclude hidden subcommands so a typo cannot reveal a subcommand name | |
| // that was intentionally kept out of --help. Hidden commands still | |
| // resolve via subIndex when invoked by exact name. | |
| const visibleKeys = []; | |
| for (const [key, sub] of subIndex) { | |
| if (!sub.hidden) visibleKeys.push(key); | |
| } | |
| const suggestions = suggest(value, visibleKeys); | |
| state.errors.push(new CliError.UnknownSubcommand({ | |
| subcommand: value, | |
| parent: commandPath, | |
| suggestions | |
| })); | |
| } | |
| return { | |
| _tag: "Argument" | |
| }; | |
| }; | |
| /* ========================================================================== */ | |
| /* Token Processing */ | |
| /* ========================================================================== */ | |
| /** | |
| * Processes a flag token: looks up in registry, consumes value, records it. | |
| * Reports unrecognized flags as errors. | |
| */ | |
| const processFlag = (token, cursor, context, state) => { | |
| const { | |
| commandPath, | |
| flagRegistry | |
| } = context; | |
| const resolved = resolveFlag(token, flagRegistry); | |
| if (!resolved) { | |
| state.errors.push(createUnrecognizedFlagError(token, flagRegistry.params, commandPath)); | |
| return; | |
| } | |
| const consumed = consumeFlagValue(cursor, token, resolved.param, resolved.negated); | |
| if (consumed._tag === "Error") { | |
| state.errors.push(consumed.error); | |
| return; | |
| } | |
| state.flags.add(resolved.param.name, consumed.value); | |
| }; | |
| /** | |
| * Processes a value token based on current parsing mode. | |
| * | |
| * In AwaitingFirstValue mode: | |
| * - Check if value is a subcommand | |
| * - If so, return SubcommandResult to exit scanning | |
| * - If not, switch to CollectingArguments mode | |
| * | |
| * In CollectingArguments mode: | |
| * - Simply add value to arguments list | |
| */ | |
| const processValue = (value, cursor, context, state) => { | |
| if (state.mode._tag === "AwaitingFirstValue") { | |
| const result = resolveFirstValue(value, cursor, context, state); | |
| if (result._tag === "Subcommand") { | |
| return result.result; | |
| } | |
| state.mode = { | |
| _tag: "CollectingArguments" | |
| }; | |
| } | |
| state.arguments.push(value); | |
| return undefined; | |
| }; | |
| /* ========================================================================== */ | |
| /* Command Level Scanning */ | |
| /* ========================================================================== */ | |
| /** | |
| * Scans a single command level, processing all tokens. | |
| * | |
| * For each token: | |
| * - Flags: Look up, consume value, record in accumulator | |
| * - Values: Check for subcommand (first value only), then collect as arguments | |
| * | |
| * Returns LeafResult if no subcommand detected, SubcommandResult otherwise. | |
| */ | |
| const scanCommandLevel = (tokens, context) => { | |
| const cursor = makeCursor(tokens); | |
| const state = createParseState(context.flagRegistry); | |
| for (let token = cursor.take(); token; token = cursor.take()) { | |
| if (isFlagToken(token)) { | |
| processFlag(token, cursor, context, state); | |
| continue; | |
| } | |
| if (token._tag === "Value") { | |
| const subResult = processValue(token.value, cursor, context, state); | |
| if (subResult) return subResult; | |
| } | |
| } | |
| return toLeafResult(state); | |
| }; | |
| //# sourceMappingURL=parser.js.map |
Xet Storage Details
- Size:
- 22 kB
- Xet hash:
- 5b85febeeea0710d74053a95d35174b5290bbe3ca374e5c232ede69a6fba7389
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.