/** * CommandDescriptor — pure-data representation of a command tree for * shell completion generation. * * @internal */ import * as Option from "../../../Option.ts" import type { Command } from "../../Command.ts" import type * as Completions from "../../Completions.ts" import * as Param from "../../Param.ts" import * as Primitive from "../../Primitive.ts" import { toImpl } from "../command.ts" // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const toFlagType = (single: Param.Single<"flag", unknown>): Completions.FlagType => { const tag = single.primitiveType._tag switch (tag) { case "Boolean": return { _tag: "Boolean" } case "Int": return { _tag: "Int" } case "Finite": return { _tag: "Finite" } case "Date": return { _tag: "Date" } case "Choice": { const keys = Primitive.getChoiceKeys(single.primitiveType) return { _tag: "Choice", values: keys ?? [] } } case "Path": return { _tag: "Path", pathType: Primitive.getPathType(single.primitiveType) ?? "either" } case "FileText": case "FileParse": case "FileSchema": return { _tag: "Path", pathType: "file" } default: return { _tag: "String" } } } const toArgumentType = (single: Param.Single<"argument", unknown>): Completions.ArgumentType => { const tag = single.primitiveType._tag switch (tag) { case "Int": return { _tag: "Int" } case "Finite": return { _tag: "Finite" } case "Date": return { _tag: "Date" } case "Choice": { const keys = Primitive.getChoiceKeys(single.primitiveType) return { _tag: "Choice", values: keys ?? [] } } case "Path": return { _tag: "Path", pathType: Primitive.getPathType(single.primitiveType) ?? "either" } case "FileText": case "FileParse": case "FileSchema": return { _tag: "Path", pathType: "file" } default: return { _tag: "String" } } } // --------------------------------------------------------------------------- // Extraction // --------------------------------------------------------------------------- /** * @internal */ export const fromCommand = ( cmd: Command.Any, inheritedFlags: ReadonlyArray = [] ): Completions.CommandDescriptor => { const impl = toImpl(cmd) const config = impl.config const flags: Array = [] const seen = new Set() // Match help's precedence: local flags first, then ancestors root to leaf. for (const [index, flag] of [...config.flags, ...inheritedFlags].entries()) { const singles = Param.extractSingleParams(flag) for (const single of singles) { if (single.kind !== "flag") continue // Omit hidden flags from completion scripts so tab-completion in the // shell does not advertise flags that are absent from --help. if (single.hidden) continue if (index >= config.flags.length && seen.has(single.name)) continue seen.add(single.name) flags.push({ name: single.name, aliases: single.aliases, description: Option.getOrUndefined(single.description), type: toFlagType(single as Param.Single<"flag", unknown>), optionalValue: single.optionalValue !== undefined, negation: single.negation }) } } const args: Array = [] for (const arg of config.arguments) { const singles = Param.extractSingleParams(arg) const metadata = Param.getParamMetadata(arg) for (const single of singles) { if (single.kind !== "argument") continue args.push({ name: single.name, description: Option.getOrUndefined(single.description), required: !metadata.isOptional, variadic: metadata.isVariadic, type: toArgumentType(single as Param.Single<"argument", unknown>) }) } } const subcommands: Array = [] const sharedFlags = [...inheritedFlags, ...impl.contextConfig.flags] for (const group of cmd.subcommands) { for (const subcommand of group.commands) { // Omit unlisted subcommands from completion scripts so tab-completion in // the shell does not advertise commands that are absent from --help. if (subcommand.unlisted) continue const descriptor = fromCommand(subcommand, sharedFlags) subcommands.push(descriptor) if (subcommand.alias && subcommand.alias !== subcommand.name) { subcommands.push({ ...descriptor, name: subcommand.alias }) } } } return { name: cmd.name, description: cmd.shortDescription ?? cmd.description, flags, arguments: args, subcommands } } /** * Whether the flag reads its value from the next word. * * @internal */ export const takesSeparateValue = (flag: Completions.FlagDescriptor): boolean => flag.type._tag !== "Boolean" && flag.optionalValue !== true /** * Long name that sets a boolean flag to `false`, or `undefined` when the flag * has no negated form. * * @internal */ export const negatedName = (flag: Completions.FlagDescriptor): string | undefined => flag.type._tag !== "Boolean" || flag.negation === false ? undefined : flag.negation ?? `no-${flag.name}`