let defaultOptions; import { getCallerFile } from "https://deno.land/x/caller_metadata@v0.0.3/src/main.ts"; import { CommandLineHelpGenerator } from "./generators/cli-generator.ts"; import { MarkdownGenerator } from "./generators/markdown-generator.ts"; import { parse } from "https://deno.land/std@0.168.0/flags/mod.ts"; import { ESCAPE_SEQUENCES } from "./ansi.ts"; export class CommandLineOptions { static collecting = !1; static _collector; static #contexts = new Map(); static defaultHelpFileURL = new URL("./RUN.md", "file://" + Deno.cwd() + "/"); static #globalLockContext; static #lockedCommands = new Map(); #contextName; #description; #optionConfigs = {}; #helpFile; static capture() { if (this._collector) return this._collector(), new Promise((resolve)=>setTimeout(resolve, 60_000)); } constructor(contextName, description, helpFile){ helpFile = new URL(helpFile ?? "./RUN.md", getCallerFile()), this.#contextName = contextName, this.#description = description, this.#helpFile = helpFile, CommandLineOptions.#contexts.set(this.#contextName, this), generatingStaticHelp && CommandLineOptions.generateHelpMarkdownFile(); } options(options, allowOtherOptions = !1) { let def = this.#getEmptyOptionParserDefinition(); for (let [name, config] of Object.entries(options))this.#registerOption("", name, config), this.#addOptionConfigToParserDefinition(name, config, def); return allowOtherOptions || (CommandLineOptions.#globalLockContext = this), this.#getArgValues(options, def, void 0, !allowOtherOptions); } command(name, options, allowOtherOptionsForCommand = !1) { CommandLineOptions.#lockedCommands.has(name) && (console.error(`${ESCAPE_SEQUENCES.RED}Cannot extend command "${name}" for "${this.#contextName}". The command is used and locked by context "${CommandLineOptions.#lockedCommands.has(name) ? CommandLineOptions.#lockedCommands.get(name).#contextName : "unknown"}". No additional command line options can be defined.${ESCAPE_SEQUENCES.RESET}`), Deno.exit(1)); let def = this.#getEmptyOptionParserDefinition(); for (let [optionName, config] of Object.entries(options ?? {}))this.#registerOption(name, optionName, config), this.#addOptionConfigToParserDefinition(optionName, config, def); return allowOtherOptionsForCommand || CommandLineOptions.#lockedCommands.set(name, this), this.#getArgValues(options, def, name, !allowOtherOptionsForCommand); } option(name, config) { let def = this.#getEmptyOptionParserDefinition(); return this.#addOptionConfigToParserDefinition(name, config, def), this.#registerOption("", name, config), this.#getArgValues({ [name]: config }, def, void 0)[name]; } #getEmptyOptionParserDefinition() { return { string: [], boolean: [], alias: {}, default: {}, collect: [] }; } #addOptionConfigToParserDefinition(name, config, def) { if (config?.type == "string" || config?.type == "number" || config?.type == "URL" ? def.string.push(name) : def.boolean.push(name), config?.aliases) for (let a of config?.aliases)def.alias[a] = name; config?.default != void 0 && (def.default[name] = config.default), config?.multiple && def.collect.push(name); } #getCollectorArgForNotPrefixedArgs(options) { let arg; for (let [name, config] of Object.entries(options))config?.collectNotPrefixedArgs && (arg && (console.error(`${ESCAPE_SEQUENCES.RED}Multiple arguments are used to collect remaining non-prefixed command line arguments: ${this.#formatArgName(name)} and ${this.#formatArgName(arg)}${ESCAPE_SEQUENCES.RESET}`), Deno.exit(1)), arg = name); return arg; } #getArgValues(options, def, command, throwOnInvalid = !1) { let valid = !0, notPrefixedArgsCollector = options ? this.#getCollectorArgForNotPrefixedArgs(options) : void 0, notPrefixedArgsCollectorConfig = options?.[notPrefixedArgsCollector], collected = []; if (command || throwOnInvalid || notPrefixedArgsCollector) { let isFirst = !0; def.unknown = (arg, key)=>(command && isFirst ? (isFirst = !1, key && (valid = !1), key || arg === command || (valid = !1)) : (notPrefixedArgsCollector && !key ? (!notPrefixedArgsCollectorConfig?.multiple && collected.length && (console.error(`${ESCAPE_SEQUENCES.RED}Too many collected arguments (${this.#formatArgName(notPrefixedArgsCollector)})${ESCAPE_SEQUENCES.RESET}`), Deno.exit(1)), collected.push(arg)) : !throwOnInvalid || showHelp || generatingStaticHelp || (console.error(`${ESCAPE_SEQUENCES.RED}Invalid command line option${command ? ` for command "${command}"` : ""}:\n${arg}${ESCAPE_SEQUENCES.RESET}`), Deno.exit(1)), isFirst = !1), !1); } let parsed = parse(Deno.args, def); if (notPrefixedArgsCollector && (parsed[notPrefixedArgsCollector] instanceof Array ? parsed[notPrefixedArgsCollector].push(...collected) : collected.length && (parsed[notPrefixedArgsCollector] = collected[0])), !valid) return null; let values = {}; for (let [name, config] of Object.entries(options ?? {})){ let val = parsed[name], isMultiple = !!config?.multiple; if (!showHelp && !generatingStaticHelp && config?.required && (!isMultiple && void 0 == val || isMultiple && !val.length)) { let [args, placeholder, description] = this.#getArg(name); console.error(`${ESCAPE_SEQUENCES.RED}Missing command line option${command ? ` for command "${command}"` : ""}:\n${CommandLineOptions.commandLineHelpGenerator.formatPrefix(args, placeholder)} ${CommandLineOptions.commandLineHelpGenerator.formatDescription(description, 2)}${ESCAPE_SEQUENCES.RESET}`), Deno.exit(1); } showHelp || generatingStaticHelp || config?.type != "string" || config?.allowEmptyString !== !1 || val && val.length ? config?.type == "number" ? values[name] = isMultiple ? val.map((v)=>this.#validateNumber(v, parsed, name)) : this.#validateNumber(val, parsed, name) : config?.type == "URL" ? values[name] = isMultiple ? val.map((v)=>this.#validateURL(v)) : this.#validateURL(val) : values[name] = val : (console.error(`${ESCAPE_SEQUENCES.RED}Invalid value for command line option ${this.#formatArgName(this.#getUsedCommandLineArgAlias(parsed, name))}: cannot be empty${ESCAPE_SEQUENCES.RESET}`), Deno.exit(1)); } return values; } #validateNumber(val, parsed, name) { return void 0 == val ? val : (String(val).match(/^[\d.]+$/) || (console.error(`${ESCAPE_SEQUENCES.RED}Invalid value for command line option ${this.#formatArgName(this.#getUsedCommandLineArgAlias(parsed, name))}: must be a number${ESCAPE_SEQUENCES.RESET}`), Deno.exit(1)), parseFloat(val)); } #validateURL(val) { return void 0 == val ? val : new URL(val, "file://" + Deno.cwd() + "/"); } #getUsedCommandLineArgAlias(parsed, name) { let nameCandidates = this.#getAliases(name, !1); for (let key of Object.keys(parsed))if (nameCandidates.includes(key)) return key; return nameCandidates[0]; } #registerOption(commandName = "", name, config) { if (CommandLineOptions.#globalLockContext && (console.error(`${ESCAPE_SEQUENCES.RED}Cannot add command line options for "${this.#contextName}". Options were locked by context "${CommandLineOptions.#globalLockContext.#contextName}". No additional command line options can be defined.${ESCAPE_SEQUENCES.RESET}`), Deno.exit(1)), !config?.overload && !showHelp && !generatingStaticHelp) { let [existingContext, optionConfig] = this.#getContextForArgument(name); for (let alias of (existingContext && existingContext != this && !optionConfig.overload && console.warn(`${ESCAPE_SEQUENCES.YELLOW}command line option ${this.#formatArgName(name)} is used by two different contexts: "${existingContext.#contextName}" and "${this.#contextName}"${ESCAPE_SEQUENCES.RESET}`), config?.aliases ?? [])){ let [existingContext, optionConfig] = this.#getContextForArgument(alias); existingContext && existingContext != this && !optionConfig.overload && console.warn(`${ESCAPE_SEQUENCES.YELLOW}command line option ${this.#formatArgName(alias)} is used by two different contexts: "${existingContext.#contextName}" and "${this.#contextName}"${ESCAPE_SEQUENCES.RESET}`); } } if (this.#optionConfigs[commandName] || (this.#optionConfigs[commandName] = {}), this.#optionConfigs[commandName][name]) { if (config) for (let [key, val] of Object.entries(config))void 0 == this.#optionConfigs[commandName][name][key] && (this.#optionConfigs[commandName][name][key] = val); } else this.#optionConfigs[commandName][name] = config; generatingStaticHelp && CommandLineOptions.generateHelpMarkdownFile(); } #getContextForArgument(arg) { for (let [_name, context] of CommandLineOptions.#contexts){ let optionConfigs = this.#getSubcommandOptions(); if (arg in optionConfigs) return [ context, optionConfigs[arg] ]; for (let opt of Object.values(optionConfigs))if (opt?.aliases?.includes(arg)) return [ context, opt ]; } return []; } *#getArgs(type, subcommand) { for (let name of Object.keys(this.#getSubcommandOptions(subcommand))){ let data = this.#getArg(name, type, subcommand); data && (yield data); } } #getArg(name, type, subcommand) { let config = this.#getSubcommandOptions(subcommand)[name]; if (!(config?._dev || "required" == type && !config?.required || "optional" == type && config?.required)) return [ this.#getAliases(name, !0, subcommand), config?.type == "boolean" ? void 0 : this.#getPlaceholder(name, subcommand), config?.description ?? "", config?.default ]; } #getAliases(name, formatted = !0, subcommand) { let config = this.#getSubcommandOptions(subcommand)[name], aliases = []; for (let a of config?.aliases ?? [])aliases.push(formatted ? this.#formatArgName(a) : a); return aliases.push(formatted ? this.#formatArgName(name) : name), aliases; } #getPlaceholder(name, subcommand) { let config = this.#getSubcommandOptions(subcommand)[name]; return config?.type !== "boolean" && config?.placeholder ? config.placeholder : null; } #getSubcommandOptions(subcommand) { let all = {}; for (let [commandName, options] of Object.entries(this.#optionConfigs))(void 0 === subcommand || commandName === subcommand) && Object.assign(all, options); return all; } #formatArgName(name) { return (1 == name.length ? "-" : "--") + name; } get #subcommands() { return Object.keys(this.#optionConfigs); } static #getStringLengthWithoutFormatters(string) { return string.replace(/\x1b\[[0-9;]*m/g, "").length; } generateHelp(generator) { let content = "", max_prefix_size = 0; for (let subcommand of (content += generator.formatTitle(this.#contextName, 2), this.#description && (content += `\n${generator.formatDescription(this.#description, 1)}\n`), this.#subcommands)){ let requiredArgs = [ ...this.#getArgs("required", subcommand) ], optionalArgs = [ ...this.#getArgs("optional", subcommand) ]; for (let [args, placeholder, description, defaultVal] of (subcommand && (content += generator.formatSubcommand(subcommand)), requiredArgs.length && optionalArgs.length && (content += generator.createSection("Required:")), requiredArgs)){ let prefix = generator.formatPrefix(args, placeholder), size = CommandLineOptions.#getStringLengthWithoutFormatters(prefix); size > max_prefix_size && (max_prefix_size = size); let defaultText = defaultVal ? generator.formatDefault(defaultVal) : ""; content += `\n${prefix}\x01${" ".repeat(generator.getMinSpacing?.() ?? 1)}${generator.formatDescription(description + defaultText, 2)}`; } for (let [args, placeholder, description, defaultVal] of (optionalArgs.length && (content += generator.createSection("\nOptional:")), optionalArgs)){ let prefix = generator.formatPrefix(args, placeholder, !0), size = CommandLineOptions.#getStringLengthWithoutFormatters(prefix); size > max_prefix_size && (max_prefix_size = size); let defaultText = defaultVal ? generator.formatDefault(defaultVal) : ""; content += `\n${prefix}\x01${" ".repeat(generator.getMinSpacing?.() ?? 1)}${generator.formatDescription(description + defaultText, 2)}`; } } return [ content, max_prefix_size ]; } generateHelpMarkdownFile(log = !0) { return !!this.#helpFile.toString().startsWith("file://") && (log && console.log("Generating help page in " + this.#helpFile.pathname + " (can be displayed with --help)"), Deno.writeTextFileSync(this.#helpFile, CommandLineOptions.generateHelp(CommandLineOptions.markdownHelpGenerator, !0)), !0); } static printHelp(keepOrder = !1) { console.log(this.generateHelp(CommandLineOptions.commandLineHelpGenerator, keepOrder)); } static generateHelp(generator, keepOrder = !1) { let defaultOptionsContent, content_array = [], max_prefix_size = 0; for (let e of keepOrder ? this.#contexts.values() : [ ...this.#contexts.values() ].toReversed()){ let [c, c_maxprefix_size] = e.generateHelp(generator); c_maxprefix_size > max_prefix_size && (max_prefix_size = c_maxprefix_size), e == defaultOptions ? defaultOptionsContent = c : content_array.push(c); } defaultOptionsContent && content_array.push(defaultOptionsContent); let content = content_array.join("\n").replace(/^.*\x01/gm, (v)=>v.replace("\x01", "").padEnd(max_prefix_size + (v.length - this.#getStringLengthWithoutFormatters(v)))); return (generator.getPreamble?.() ?? "") + content + (generator.getEnd?.() ?? ""); } static #generating = !1; static generateHelpMarkdownFile(log = !0) { this.#generating || (this.#generating = !0, setTimeout(()=>{ for (let ctx of (this.#generating = !1, this.#contexts.size || console.error(`${ESCAPE_SEQUENCES.RED}Cannot create Help file, no command line options registered${ESCAPE_SEQUENCES.RESET}`), [ ...this.#contexts.values() ].toReversed()))if ("file:" === ctx.#helpFile.protocol) return void ctx.generateHelpMarkdownFile(log); [ ...this.#contexts.values() ][0].generateHelpMarkdownFile(log); }, 1000)); } static parseHelpMarkdownFiles() { return this.parseHelpMarkdownFile(this.defaultHelpFileURL); } static parseHelpMarkdownFile(file) { try { let entries = Deno.readTextFileSync(file).split(/^## /gm); for (let e of (MarkdownGenerator.generalDescription = entries.shift()?.trim() ?? MarkdownGenerator.generalDescription, entries)){ let parts = e.split(/\n+/), name = parts.shift(); if (!name) continue; let description = ""; for(; !parts[0]?.startsWith("#") && !parts[0]?.startsWith(" *") && !parts[0]?.startsWith("Required:") && !parts[0]?.startsWith("Optional:");)description += (description ? "\n" : "") + parts.shift(); let c = this.#contexts.get(name) ?? new CommandLineOptions(name, description || void 0), required = !0, currentCommand = ""; for (let part of parts){ let placeholder; if (part.startsWith("###")) { currentCommand = part.replace("###", "").trim(); continue; } if (part.startsWith("Required:")) continue; if (part.startsWith("Optional:")) { required = !1; continue; } if (!part.trim().startsWith("*")) continue; let line = part.match(/`(.*)` *(.*$)/); if (!line) continue; let description = line[2], aliases = line[1]?.split(",")?.map((a)=>{ let parts = a.trim().split(" "); return parts[1] && (placeholder = parts[1]), parts[0].replace(/^\-+/, ""); }); if (!aliases) continue; let name = aliases.pop(); name && c.#registerOption(currentCommand, name, { aliases, required, placeholder, description }); } } return !0; } catch { return !1; } } static commandLineHelpGenerator = new CommandLineHelpGenerator(); static markdownHelpGenerator = new MarkdownGenerator(); } let generatingStaticHelp = !1, showHelp = !1; globalThis.Deno && (defaultOptions = new CommandLineOptions("Other Options", void 0, CommandLineOptions.defaultHelpFileURL), showHelp = CommandLineOptions.collecting = !!defaultOptions.option("help", { type: "boolean", aliases: [ "h" ], description: "Show the help page" }), (generatingStaticHelp = !!defaultOptions.option("generate-help", { type: "boolean", _dev: !0, description: "Run the program with this option to update this help page" })) ? (CommandLineOptions.collecting = !0, addEventListener("load", CommandLineOptions._collector = ()=>{ CommandLineOptions.generateHelpMarkdownFile(); })) : showHelp && (CommandLineOptions.parseHelpMarkdownFiles() ? (CommandLineOptions.printHelp(!0), Deno.exit(0)) : (CommandLineOptions.collecting = !0, addEventListener("load", CommandLineOptions._collector = ()=>{ CommandLineOptions.printHelp(!0), Deno.exit(0); })))); //# sourceMappingURL=./main.ts.map