import chalk from 'https://deno.land/std@0.85.0/fmt/colors.ts'; import { ParsingInto, ArgParser, ParsingError, ParsingResult, ParseContext, } from './argparser.ts'; import { AstNode } from './newparser/parser.ts'; import { PrintHelp, ProvidesHelp, Versioned, Named, Descriptive, Aliased, } from './helpdoc.ts'; import { padNoAnsi, entries, groupBy, flatMap } from './utils.ts'; import { Runner } from './runner.ts'; import { createCircuitBreaker, handleCircuitBreaker } from './circuitbreaker.ts'; import * as Result from './Result.ts'; type ArgTypes = Record & Partial>; type HandlerFunc = (args: Output) => any; type CommandConfig< Arguments extends ArgTypes, Handler extends HandlerFunc > = { args: Arguments; version?: string; name: string; description?: string; handler: Handler; aliases?: string[]; }; type Output = { [key in keyof Args]: ParsingInto; }; /** * A command line utility. * * A combination of multiple flags, options and arguments * with a common name and a handler that expects them as input. */ export function command< Arguments extends ArgTypes, Handler extends HandlerFunc >( config: CommandConfig ): ArgParser> & PrintHelp & ProvidesHelp & Named & Runner, ReturnType> & Partial { const argEntries = entries(config.args); const circuitbreaker = createCircuitBreaker(!!config.version); return { name: config.name, aliases: config.aliases, handler: config.handler, description: config.description, version: config.version, helpTopics() { return flatMap( Object.values(config.args).concat([circuitbreaker]), x => x.helpTopics?.() ?? [] ); }, printHelp(context) { const lines: string[] = []; let name = context.hotPath?.join(' ') ?? ''; if (!name) { name = config.name; } name = chalk.bold(name); if (config.version) { name += ' ' + chalk.dim(config.version); } lines.push(name); if (config.description) { lines.push(chalk.dim('> ') + config.description); } const usageBreakdown = groupBy(this.helpTopics(), x => x.category); for (const [category, helpTopics] of entries(usageBreakdown)) { lines.push(''); lines.push(category.toUpperCase() + ':'); const widestUsage = helpTopics.reduce((len, curr) => { return Math.max(len, curr.usage.length); }, 0); for (const helpTopic of helpTopics) { let line = ''; line += ' ' + padNoAnsi(helpTopic.usage, widestUsage, 'end'); line += ' - '; line += helpTopic.description; for (const defaultValue of helpTopic.defaults) { line += chalk.dim(` [${defaultValue}]`); } lines.push(line); } } return lines.join('\n'); }, register(opts) { for (const [, arg] of argEntries) { arg.register?.(opts); } }, async parse( context: ParseContext ): Promise>> { if (context.hotPath?.length === 0) { context.hotPath.push(config.name); } const resultObject = {} as Output; const errors: ParsingError[] = []; for (const [argName, arg] of argEntries) { const result = await arg.parse(context); if (Result.isErr(result)) { errors.push(...result.error.errors); } else { resultObject[argName] = result.value; } } const unknownArguments: AstNode[] = []; for (const node of context.nodes) { if (context.visitedNodes.has(node)) { continue; } if (node.type === 'forcePositional') { // A `forcePositional` node can't really be visited since it has no meaning // other than forcing a positional argument in the parsing phase continue; } else if (node.type === 'shortOptions') { for (const option of node.options) { if (context.visitedNodes.has(option)) { continue; } unknownArguments.push(option); } } else { unknownArguments.push(node); } } if (unknownArguments.length > 0) { errors.push({ message: 'Unknown arguments', nodes: unknownArguments, }); } if (errors.length > 0) { return Result.err({ errors: errors, partialValue: resultObject, }); } else { return Result.ok(resultObject); } }, async run(context) { const parsed = await this.parse(context); const breaker = await circuitbreaker.parse(context); handleCircuitBreaker(context, this, breaker); if (Result.isErr(parsed)) { return Result.err(parsed.error); } return Result.ok(await this.handler(parsed.value)); }, }; }