'use strict'; const color = require('@breadc/color'); class BreadcError extends Error { } class ParseError extends Error { } function camelCase(text) { return text.split("-").map((t, idx) => idx === 0 ? t : t[0].toUpperCase() + t.slice(1)).join(""); } function twoColumn(texts, split = " ") { const left = padRight(texts.map((t) => t[0])); return left.map((l, idx) => l + split + texts[idx][1]); } function padRight(texts, fill = " ") { const length = texts.map((t) => t.length).reduce((max, l) => Math.max(max, l), 0); return texts.map((t) => t + fill.repeat(length - t.length)); } const OptionRE = /^(-[a-zA-Z], )?--([a-zA-Z0-9\-]+)( <[a-zA-Z0-9\-]+>)?$/; function makeOption(format, config = { default: void 0 }) { let name = ""; let short = void 0; const match = OptionRE.exec(format); if (match) { name = match[2]; if (match[1]) { short = match[1][1]; } if (match[3]) { if (name.startsWith("no-")) { throw new BreadcError(`Can not parse option format (${format})`); } const initial = config.default ?? void 0; return { format, type: "string", name, short, description: config.description ?? "", order: 0, // @ts-ignore initial: config.cast ? config.cast(initial) : initial, cast: config.cast }; } else { if (name.startsWith("no-")) { name = name.slice(3); config.default = true; } const initial = config.default === void 0 || config.default === null ? false : config.default; return { format, type: "boolean", name, short, description: config.description ?? "", order: 0, // @ts-ignore initial: config.cast ? config.cast(initial) : initial, cast: config.cast }; } } else { throw new BreadcError(`Can not parse option format (${format})`); } } const initContextOptions = (options, context) => { for (const option of options) { context.options.set(option.name, option); if (option.short) { context.options.set(option.short, option); } if (option.type === "boolean") { context.options.set("no-" + option.name, option); } if (option.initial !== void 0) { context.result.options[camelCase(option.name)] = option.initial; } } }; var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; class Token { constructor(text) { __publicField(this, "text"); __publicField(this, "_type"); this.text = text; } /** * @returns Raw argument text */ raw() { return this.text; } /** * @returns Number representation */ number() { return Number(this.text); } /** * @returns Remove start - for long or short option */ option() { return this.text.replace(/^-+/, ""); } isOption() { return this.type() === "long" || this._type === "short"; } isText() { return this.type() === "number" || this._type === "string"; } type() { if (this._type) { return this._type; } else if (this.text === "--") { return this._type = "--"; } else if (this.text === "-") { return this._type = "-"; } else if (!isNaN(Number(this.text))) { return this._type = "number"; } else if (this.text.startsWith("--")) { return this._type = "long"; } else if (this.text.startsWith("-")) { return this._type = "short"; } else { return this._type = "string"; } } /* c8 ignore next 1 */ } class Lexer { constructor(rawArgs) { __publicField(this, "rawArgs"); __publicField(this, "cursor", 0); this.rawArgs = rawArgs; } next() { const value = this.rawArgs[this.cursor]; this.cursor += 1; return value ? new Token(value) : void 0; } hasNext() { return this.cursor < this.rawArgs.length; } peek() { const value = this.rawArgs[this.cursor]; return value ? new Token(value) : void 0; } [Symbol.iterator]() { const that = this; return { next() { const value = that.rawArgs[that.cursor]; that.cursor += 1; return { value: value !== void 0 ? new Token(value) : void 0, done: that.cursor > that.rawArgs.length }; } }; } } function makeTreeNode(pnode) { const node = { children: /* @__PURE__ */ new Map(), init() { }, next(token, context) { const t = token.raw(); context.result["--"].push(t); if (node.children.has(t)) { const next = node.children.get(t); next.init(context); return next; } else { return node; } }, finish() { return pnode.command?.callback; }, ...pnode }; return node; } function parseOption(cursor, token, context) { const o = token.option(); const [key, rawV] = o.split("="); if (context.options.has(key)) { const option = context.options.get(key); const name = camelCase(option.name); if (option.parse) { return option.parse(cursor, token, context); } else if (option.type === "boolean") { const negative = key.startsWith("no-"); if (rawV === void 0 || ["true", "yes", "t", "y"].includes(rawV.toLowerCase())) { context.result.options[name] = !negative ? true : false; } else if (["false", "no", "f", "n"].includes(rawV.toLowerCase())) { context.result.options[name] = !negative ? false : true; } else { throw new ParseError(`Unexpected value ${rawV} for ${option.format}`); } } else if (option.type === "string") { if (rawV !== void 0) { context.result.options[name] = rawV; } else { const value = context.lexer.next(); if (value !== void 0 && !value.isOption()) { context.result.options[name] = value.raw(); } else { throw new ParseError( `You should provide arguments for ${option.format}` ); } } } else { throw new ParseError("unreachable"); } if (option.cast) { context.result.options[name] = option.cast(context.result.options[name]); } } else { switch (context.config.allowUnknownOption) { case "rest": context.result["--"].push(token.raw()); case "skip": break; case "error": default: throw new ParseError(`Unknown option ${token.raw()}`); } } return cursor; } function parse(root, args) { const lexer = new Lexer(args); const context = { lexer, options: /* @__PURE__ */ new Map(), result: { arguments: [], options: {}, "--": [] }, meta: {}, config: { allowUnknownOption: "error" }, parseOption }; let cursor = root; root.init(context); for (const token of lexer) { if (token.type() === "--") { break; } else if (token.isOption()) { const res = context.parseOption(cursor, token, context); if (res === false) { break; } else { cursor = res; } } else if (token.isText()) { const res = cursor.next(token, context); if (res === false) { break; } else { cursor = res; } } else { throw new ParseError("unreachable"); } } const callback = cursor.finish(context); for (const token of lexer) { context.result["--"].push(token.raw()); } return { callback, matched: { node: cursor, command: cursor.command, option: cursor.option }, meta: context.meta, arguments: context.result.arguments, options: context.result.options, "--": context.result["--"] }; } function makeCommand(format, config, root, container) { const args = []; const options = []; const command = { callback: void 0, format, description: config.description ?? "", _default: false, _arguments: args, _options: options, option(format2, _config, _config2 = {}) { const config2 = typeof _config === "string" ? { description: _config, ..._config2 } : _config; const option = makeOption(format2, config2); options.push(option); return command; }, alias(format2) { const aliasArgs = []; const node2 = makeNode(aliasArgs); function* g() { for (const f of format2.split(" ")) { yield { type: "const", name: f }; } for (const a of args.filter((a2) => a2.type !== "const")) { yield a; } return void 0; } insertTreeNode(aliasArgs, node2, g()); return command; }, action(fn) { command.callback = async (parsed) => { await container.preCommand(command, parsed); const result = await fn(...parsed.arguments, { ...parsed.options, "--": parsed["--"] }); await container.postCommand(command, parsed); return result; }; } }; const node = makeNode(args); insertTreeNode(args, node, parseCommandFormat(format)); return command; function makeNode(args2) { return makeTreeNode({ command, init(context) { context.config.allowUnknownOption = config.allowUnknownOption ?? "error"; initContextOptions(options, context); }, finish(context) { const rest = context.result["--"]; for (let i = 0; i < args2.length; i++) { if (args2[i].type === "const") { if (rest[i] !== args2[i].name) { throw new ParseError(`Sub-command ${args2[i].name} mismatch`); } } else if (args2[i].type === "require") { if (i >= rest.length) { throw new ParseError( `You must provide require argument ${args2[i].name}` ); } context.result.arguments.push(rest[i]); } else if (args2[i].type === "optional") { context.result.arguments.push(rest[i]); } else if (args2[i].type === "rest") { context.result.arguments.push(rest.splice(i)); } } context.result["--"] = rest.splice(args2.length); return command.callback; } }); } function insertTreeNode(args2, node2, parsed) { let cursor = root; for (const arg of parsed) { args2.push(arg); if (arg.type === "const") { const name = arg.name; if (cursor.children.has(name)) { cursor = cursor.children.get(name); } else { const internalNode = makeTreeNode({ next(token, context) { const t = token.raw(); context.result["--"].push(t); if (internalNode.children.has(t)) { const next = internalNode.children.get(t); next.init(context); return next; } else { throw new ParseError(`Unknown sub-command (${t})`); } }, finish() { throw new ParseError(`Unknown sub-command`); } }); cursor.children.set(name, internalNode); cursor = internalNode; } } } cursor.command = command; if (cursor !== root) { for (const [key, value] of cursor.children) { node2.children.set(key, value); } cursor.children = node2.children; cursor.next = node2.next; cursor.init = node2.init; cursor.finish = node2.finish; } else { command._default = true; cursor.finish = node2.finish; } } } function* parseCommandFormat(format) { let state = 0; for (let i = 0; i < format.length; i++) { if (format[i] === "<") { if (state !== 0 && state !== 1) { throw new BreadcError( `Required arguments should be placed before optional or rest arguments` ); } const start = i; while (i < format.length && format[i] !== ">") { i++; } const name = format.slice(start + 1, i); state = 1; yield { type: "require", name }; } else if (format[i] === "[") { if (state !== 0 && state !== 1) { throw new BreadcError( `There is at most one optional or rest arguments` ); } const start = i; while (i < format.length && format[i] !== "]") { i++; } const name = format.slice(start + 1, i); state = 2; if (name.startsWith("...")) { yield { type: "rest", name }; } else { yield { type: "optional", name }; } } else if (format[i] !== " ") { if (state !== 0) { throw new BreadcError(`Sub-command should be placed at the beginning`); } const start = i; while (i < format.length && format[i] !== " ") { i++; } const name = format.slice(start, i); state = 0; yield { type: "const", name }; } } return void 0; } function makePluginContainer(plugins = []) { const onPreCommand = {}; const onPostCommand = {}; for (const plugin of plugins) { if (typeof plugin.onPreCommand === "function") { const key = "*"; if (!(key in onPreCommand)) { onPreCommand[key] = []; } onPreCommand[key].push(plugin.onPreCommand); } else { for (const [key, fn] of Object.entries(plugin.onPreCommand ?? {})) { if (!(key in onPreCommand)) { onPreCommand[key] = []; } onPreCommand[key].push(fn); } } if (typeof plugin.onPostCommand === "function") { const key = "*"; if (!(key in onPostCommand)) { onPostCommand[key] = []; } onPostCommand[key].push(plugin.onPostCommand); } else { for (const [key, fn] of Object.entries(plugin.onPostCommand ?? {})) { if (!(key in onPostCommand)) { onPostCommand[key] = []; } onPostCommand[key].push(fn); } } } const run = async (container, command, result) => { const prefix = command._arguments.filter((a) => a.type === "const").map((a) => a.name); if (prefix.length === 0) { prefix.push("_"); } for (let i = 0; i <= prefix.length; i++) { const key = i === 0 ? "*" : prefix.slice(0, i).map( (t, idx) => idx === 0 ? t : t[0].toUpperCase() + t.slice(1) ).join(""); const fns = container[key]; if (fns && fns.length > 0) { await Promise.all(fns.map((fn) => fn(result))); } } }; return { init(breadc, allCommands, globalOptions) { if (plugins.length === 0) return; for (const p of plugins) { p.onInit?.(breadc, allCommands, globalOptions); } }, async preRun(breadc) { if (plugins.length === 0) return; for (const p of plugins) { await p.onPreRun?.(breadc); } }, async preCommand(command, result) { if (plugins.length === 0) return; await run(onPreCommand, command, result); }, async postCommand(command, result) { if (plugins.length === 0) return; await run(onPostCommand, command, result); }, async postRun(breadc) { if (plugins.length === 0) return; for (const p of plugins) { await p.onPostRun?.(breadc); } } }; } function definePlugin(plugin) { return plugin; } function makeVersionCommand(name, config) { let description = "Print version"; if (typeof config.builtin?.version === "object") { if (config.builtin.version.description) { description = config.builtin.version.description; } } const node = makeTreeNode({ next() { return false; }, finish() { return () => { const text = typeof config.builtin?.version === "object" && config.builtin.version.content ? config.builtin.version.content : `${name}/${config.version ? config.version : "unknown"}`; console.log(text); return text; }; } }); const option = { format: "-v, --version", name: "version", short: "v", type: "boolean", initial: void 0, order: 999999999 + 1, description, parse() { return node; } }; return option; } function makeHelpCommand(name, config, allCommands) { function expandMessage(message) { const result = []; for (const row of message) { if (typeof row === "function") { const r = row(); if (r) { result.push(...expandMessage(r)); } } else if (typeof row === "string") { result.push(row); } else if (Array.isArray(row)) { const lines = twoColumn(row); for (const line of lines) { result.push(line); } } } return result; } function expandCommands(cursor) { const visited = /* @__PURE__ */ new WeakSet(); const added = /* @__PURE__ */ new WeakSet(); const commands = cursor.command ? [cursor.command] : []; const q = [cursor]; visited.add(cursor); for (let i = 0; i < q.length; i++) { const cur = q[i]; for (const [_key, cmd] of cur.children) { if (!visited.has(cmd)) { visited.add(cmd); if (cmd.command && !added.has(cmd.command)) { added.add(cmd.command); commands.push(cmd.command); } q.push(cmd); } } } const alias = /* @__PURE__ */ new Map(); for (const cmd of commands) { if (!alias.has(cmd.format)) { alias.set(cmd.format, cmd); } } return [...alias.values()]; } let description = "Print help"; if (typeof config.builtin?.help === "object") { if (config.builtin.help.description) { description = config.builtin.help.description; } } const node = makeTreeNode({ next() { return false; }, finish(context) { return () => { const cursor = context.meta.__cursor__; const usage = allCommands.length === 0 ? `[OPTIONS]` : allCommands.length === 1 ? `[OPTIONS] ${allCommands[0].format}` : allCommands.some((c) => c._default) ? `[OPTIONS] [COMMAND]` : `[OPTIONS] `; const output = [ `${name}/${config.version ? config.version : "unknown"}`, () => { if (config.description) { return ["", config.description]; } else { return void 0; } }, "", `${color.bold(color.underline("Usage:"))} ${color.bold(name)} ${usage}`, () => { const cmds = expandCommands(cursor); if (cmds.length > 0) { return [ "", color.bold(color.underline("Commands:")), cmds.map((cmd) => [ ` ${color.bold(name)} ${color.bold(cmd.format)}`, cmd.description ]) ]; } else { return void 0; } }, "", color.bold(color.underline("Options:")), [...context.options.entries()].filter(([key, op]) => key === op.name).sort((lhs, rhs) => lhs[1].order - rhs[1].order).map(([_key, op]) => [ " " + (!op.short ? " " : "") + color.bold(op.format), op.description ]), "" ]; const text = expandMessage(output).join("\n"); console.log(text); return text; }; } }); const option = { format: "-h, --help", name: "help", short: "h", type: "boolean", initial: void 0, description, order: 999999999, parse(cursor, _token, context) { context.meta.__cursor__ = cursor; return node; } }; return option; } function breadc(name, config = {}) { let defaultCommand = void 0; const allCommands = []; const globalOptions = []; if (config.builtin?.help !== false) { globalOptions.push(makeHelpCommand(name, config, allCommands)); } if (config.builtin?.version !== false) { globalOptions.push(makeVersionCommand(name, config)); } const container = makePluginContainer(config.plugins); const root = makeTreeNode({ init(context) { initContextOptions(globalOptions, context); if (defaultCommand) { initContextOptions(defaultCommand._options, context); } } }); const breadc2 = { name, description: config.description ?? "", option(format, _config, _config2 = {}) { const config2 = typeof _config === "string" ? { description: _config, ..._config2 } : _config; const option = makeOption(format, config2); globalOptions.push(option); return breadc2; }, command(text, _config = {}, _config2 = {}) { const config2 = typeof _config === "string" ? { description: _config, ..._config2 } : _config; const command = makeCommand(text, config2, root, container); if (command._default) { defaultCommand = command; } allCommands.push(command); return command; }, parse(args) { return parse(root, args); }, async run(args) { const result = breadc2.parse(args); const callback = result.callback; if (callback) { await container.preRun(breadc2); const r = await callback(result); await container.postRun(breadc2); return r; } return void 0; } }; container.init(breadc2, allCommands, globalOptions); return breadc2; } exports.BreadcError = BreadcError; exports.ParseError = ParseError; exports.breadc = breadc; exports.definePlugin = definePlugin; exports.makeTreeNode = makeTreeNode;