@optique/core
Version:
Type-safe combinatorial command-line interface parser
403 lines (401 loc) • 13.4 kB
JavaScript
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;