UNPKG

@optique/core

Version:

Type-safe combinatorial command-line interface parser

403 lines (401 loc) 13.4 kB
const require_message = require('./message.cjs'); const require_usage = require('./usage.cjs'); const require_doc = require('./doc.cjs'); const require_valueparser = require('./valueparser.cjs'); const require_parser = require('./parser.cjs'); //#region src/facade.ts /** * Creates help parsers based on the specified mode. */ function createHelpParser(mode) { const helpCommand = require_parser.command("help", require_parser.multiple(require_parser.argument(require_valueparser.string({ metavar: "COMMAND" }))), { description: require_message.message`Show help information.` }); const helpOption = require_parser.flag("--help", { description: require_message.message`Show help information.` }); const _contextualHelpParser = require_parser.object({ help: require_parser.constant(true), version: require_parser.constant(false), commands: require_parser.multiple(require_parser.argument(require_valueparser.string({ metavar: "COMMAND", pattern: /^[^-].*$/ }))), __help: require_parser.flag("--help") }); switch (mode) { case "command": return { helpCommand, helpOption: null, contextualHelpParser: null }; case "option": return { helpCommand: null, helpOption, contextualHelpParser: null }; case "both": return { helpCommand, helpOption, contextualHelpParser: null }; } } /** * Creates version parsers based on the specified mode. */ function createVersionParser(mode) { const versionCommand = require_parser.command("version", require_parser.object({}), { description: require_message.message`Show version information.` }); const versionOption = require_parser.flag("--version", { description: require_message.message`Show version information.` }); switch (mode) { case "command": return { versionCommand, versionOption: null }; case "option": return { versionCommand: null, versionOption }; case "both": return { versionCommand, versionOption }; } } /** * Systematically combines the original parser with help and version parsers. */ function combineWithHelpVersion(originalParser, helpParsers, versionParsers) { const parsers = []; if (helpParsers.helpOption) { const lenientHelpParser = { $valueType: [], $stateType: [], priority: 200, usage: helpParsers.helpOption.usage, initialState: null, parse(context) { const { buffer, optionsTerminated } = context; if (optionsTerminated) return { success: false, error: require_message.message`Options terminated`, consumed: 0 }; let helpFound = false; let helpIndex = -1; let helpCount = 0; let versionIndex = -1; for (let i = 0; i < buffer.length; i++) { if (buffer[i] === "--") break; if (buffer[i] === "--help") { helpFound = true; helpIndex = i; helpCount++; } if (buffer[i] === "--version") versionIndex = i; } if (helpFound && versionIndex > helpIndex) return { success: false, error: require_message.message`Version option wins`, consumed: 0 }; if (helpFound) return { success: true, next: { ...context, buffer: [], state: { help: true, version: false, commands: [], helpFlag: true } }, consumed: buffer.slice(0) }; return { success: false, error: require_message.message`Flag --help not found`, consumed: 0 }; }, complete(state) { return { success: true, value: state }; }, getDocFragments(state) { return helpParsers.helpOption?.getDocFragments(state) ?? { fragments: [] }; } }; parsers.push(lenientHelpParser); } if (versionParsers.versionOption) { const lenientVersionParser = { $valueType: [], $stateType: [], priority: 200, usage: versionParsers.versionOption.usage, initialState: null, parse(context) { const { buffer, optionsTerminated } = context; if (optionsTerminated) return { success: false, error: require_message.message`Options terminated`, consumed: 0 }; let versionFound = false; let versionIndex = -1; let versionCount = 0; let helpIndex = -1; for (let i = 0; i < buffer.length; i++) { if (buffer[i] === "--") break; if (buffer[i] === "--version") { versionFound = true; versionIndex = i; versionCount++; } if (buffer[i] === "--help") helpIndex = i; } if (versionFound && helpIndex > versionIndex) return { success: false, error: require_message.message`Help option wins`, consumed: 0 }; if (versionFound) return { success: true, next: { ...context, buffer: [], state: { help: false, version: true, versionFlag: true } }, consumed: buffer.slice(0) }; return { success: false, error: require_message.message`Flag --version not found`, consumed: 0 }; }, complete(state) { return { success: true, value: state }; }, getDocFragments(state) { return versionParsers.versionOption?.getDocFragments(state) ?? { fragments: [] }; } }; parsers.push(lenientVersionParser); } if (versionParsers.versionCommand) parsers.push(require_parser.object({ help: require_parser.constant(false), version: require_parser.constant(true), result: versionParsers.versionCommand, helpFlag: helpParsers.helpOption ? require_parser.optional(helpParsers.helpOption) : require_parser.constant(false) })); if (helpParsers.helpCommand) parsers.push(require_parser.object({ help: require_parser.constant(true), version: require_parser.constant(false), commands: helpParsers.helpCommand })); if (helpParsers.contextualHelpParser) parsers.push(helpParsers.contextualHelpParser); parsers.push(require_parser.object({ help: require_parser.constant(false), version: require_parser.constant(false), result: originalParser })); if (parsers.length === 1) return parsers[0]; else if (parsers.length === 2) return require_parser.longestMatch(parsers[0], parsers[1]); else return require_parser.longestMatch(...parsers); } /** * Classifies the parsing result into a discriminated union for cleaner handling. */ function classifyResult(result, args) { if (!result.success) return { type: "error", error: result.error }; const value = result.value; if (typeof value === "object" && value != null && "help" in value && "version" in value) { const parsedValue = value; const hasVersionOption = args.includes("--version"); const hasVersionCommand = args.length > 0 && args[0] === "version"; const hasHelpOption = args.includes("--help"); const hasHelpCommand = args.length > 0 && args[0] === "help"; if (hasVersionOption && hasHelpOption && !hasVersionCommand && !hasHelpCommand) {} if (hasVersionCommand && hasHelpOption && parsedValue.helpFlag) return { type: "help", commands: ["version"] }; if (parsedValue.help && (hasHelpOption || hasHelpCommand)) { let commandContext = []; if (Array.isArray(parsedValue.commands)) commandContext = parsedValue.commands; else if (typeof parsedValue.commands === "object" && parsedValue.commands != null && "length" in parsedValue.commands) commandContext = parsedValue.commands; return { type: "help", commands: commandContext }; } if ((hasVersionOption || hasVersionCommand) && (parsedValue.version || parsedValue.versionFlag)) return { type: "version" }; return { type: "success", value: parsedValue.result ?? value }; } return { type: "success", value }; } /** * Runs a parser against command-line arguments with built-in help and error * handling. * * This function provides a complete CLI interface by automatically handling * help commands/options and displaying formatted error messages with usage * information when parsing fails. It augments the provided parser with help * functionality based on the configuration options. * * The function will: * * 1. Add help command/option support (unless disabled) * 2. Parse the provided arguments * 3. Display help if requested * 4. Show formatted error messages with usage/help info on parse failures * 5. Return the parsed result or invoke the appropriate callback * * @template TParser The parser type being run. * @template THelp Return type when help is shown (defaults to `void`). * @template TError Return type when an error occurs (defaults to `never`). * @param parser The parser to run against the command-line arguments. * @param programName Name of the program used in usage and help output. * @param args Command-line arguments to parse (typically from * `process.argv.slice(2)` on Node.js or `Deno.args` on Deno). * @param options Configuration options for output formatting and callbacks. * @returns The parsed result value, or the return value of `onHelp`/`onError` * callbacks. * @throws {RunError} When parsing fails and no `onError` callback is provided. */ function run(parser, programName, args, options = {}) { const helpMode = options.help?.mode ?? "option"; const onHelp = options.help?.onShow ?? (() => ({})); const versionMode = options.version?.mode ?? "option"; const versionValue = options.version?.value ?? ""; const onVersion = options.version?.onShow ?? (() => ({})); let { colors, maxWidth, showDefault, aboveError = "usage", onError = () => { throw new RunError("Failed to parse command line arguments."); }, stderr = console.error, stdout = console.log, brief, description, footer } = options; const help = options.help ? helpMode : "none"; const version = options.version ? versionMode : "none"; const helpParsers = help === "none" ? { helpCommand: null, helpOption: null, contextualHelpParser: null } : createHelpParser(help); const versionParsers = version === "none" ? { versionCommand: null, versionOption: null } : createVersionParser(version); const augmentedParser = help === "none" && version === "none" ? parser : combineWithHelpVersion(parser, helpParsers, versionParsers); const result = require_parser.parse(augmentedParser, args); const classified = classifyResult(result, args); switch (classified.type) { case "success": return classified.value; case "version": stdout(versionValue); try { return onVersion(0); } catch { return onVersion(); } case "help": { let helpGeneratorParser; const helpAsCommand = help === "command" || help === "both"; const versionAsCommand = version === "command" || version === "both"; if (helpAsCommand && versionAsCommand) { const tempHelpParsers = createHelpParser(help); const tempVersionParsers = createVersionParser(version); if (tempHelpParsers.helpCommand && tempVersionParsers.versionCommand) helpGeneratorParser = require_parser.longestMatch(parser, tempHelpParsers.helpCommand, tempVersionParsers.versionCommand); else if (tempHelpParsers.helpCommand) helpGeneratorParser = require_parser.longestMatch(parser, tempHelpParsers.helpCommand); else if (tempVersionParsers.versionCommand) helpGeneratorParser = require_parser.longestMatch(parser, tempVersionParsers.versionCommand); else helpGeneratorParser = parser; } else if (helpAsCommand) { const tempHelpParsers = createHelpParser(help); if (tempHelpParsers.helpCommand) helpGeneratorParser = require_parser.longestMatch(parser, tempHelpParsers.helpCommand); else helpGeneratorParser = parser; } else if (versionAsCommand) { const tempVersionParsers = createVersionParser(version); if (tempVersionParsers.versionCommand) helpGeneratorParser = require_parser.longestMatch(parser, tempVersionParsers.versionCommand); else helpGeneratorParser = parser; } else helpGeneratorParser = parser; const doc = require_parser.getDocPage(helpGeneratorParser, classified.commands); if (doc != null) { const augmentedDoc = { ...doc, brief: brief ?? doc.brief, description: description ?? doc.description, footer: footer ?? doc.footer }; stdout(require_doc.formatDocPage(programName, augmentedDoc, { colors, maxWidth, showDefault })); } try { return onHelp(0); } catch { return onHelp(); } } case "error": break; } if (aboveError === "help") { const doc = require_parser.getDocPage(args.length < 1 ? augmentedParser : parser, args); if (doc == null) aboveError = "usage"; else { const augmentedDoc = { ...doc, brief: brief ?? doc.brief, description: description ?? doc.description, footer: footer ?? doc.footer }; stderr(require_doc.formatDocPage(programName, augmentedDoc, { colors, maxWidth, showDefault })); } } if (aboveError === "usage") stderr(`Usage: ${indentLines(require_usage.formatUsage(programName, augmentedParser.usage, { colors, maxWidth: maxWidth == null ? void 0 : maxWidth - 7, expandCommands: true }), 7)}`); const errorMessage = require_message.formatMessage(classified.error, { colors, quotes: !colors }); stderr(`Error: ${errorMessage}`); return onError(1); } /** * An error class used to indicate that the command line arguments * could not be parsed successfully. */ var RunError = class extends Error { constructor(message$1) { super(message$1); this.name = "RunError"; } }; function indentLines(text, indent) { return text.split("\n").join("\n" + " ".repeat(indent)); } //#endregion exports.RunError = RunError; exports.run = run;