| |
| import type { Command, Option } from "commander"; |
| import { buildParseArgv } from "../argv.js"; |
|
|
| function getCommandPathFromRoot(command: Command | undefined): Command[] { |
| const path: Command[] = []; |
| let current = command; |
| while (current?.parent) { |
| if (current.name()) { |
| path.unshift(current); |
| } |
| current = current.parent; |
| } |
| return path; |
| } |
|
|
| function findRootCommand(cmd: Command): Command { |
| let current: Command = cmd; |
| while (current.parent) { |
| current = current.parent; |
| } |
| return current; |
| } |
|
|
| function findOption(command: Command, token: string): Option | undefined { |
| const equalsIndex = token.indexOf("="); |
| const flag = equalsIndex === -1 ? token : token.slice(0, equalsIndex); |
| const exactOption = command.options.find( |
| (candidate) => |
| (candidate.short === flag || candidate.long === flag) && |
| (equalsIndex === -1 || candidate.required || candidate.optional), |
| ); |
| if (exactOption || !token.startsWith("-") || token.startsWith("--") || token.length <= 2) { |
| return exactOption; |
| } |
| |
| let claimedOption: Option | undefined; |
| for (let index = 1; index < token.length; index += 1) { |
| const option = command.options.find((candidate) => candidate.short === `-${token[index]}`); |
| if (!option) { |
| return claimedOption; |
| } |
| claimedOption ??= option; |
| if (option.required || option.optional || index === token.length - 1) { |
| return option; |
| } |
| } |
| return undefined; |
| } |
|
|
| function findNearestOption(commands: readonly Command[], token: string): Option | undefined { |
| for (let index = commands.length - 1; index >= 0; index -= 1) { |
| const command = commands[index]; |
| const option = command ? findOption(command, token) : undefined; |
| if (option) { |
| return option; |
| } |
| } |
| return undefined; |
| } |
|
|
| function matchesCommandName(command: Command, token: string): boolean { |
| return command.name() === token || command.aliases().includes(token); |
| } |
|
|
| |
| function optionTokenCount(option: Option, argv: readonly string[], index: number): number { |
| const token = argv[index] ?? ""; |
| const shortFlagIndex = |
| option.short !== undefined && !token.startsWith("--") |
| ? token.indexOf(option.short.slice(1), 1) |
| : -1; |
| const hasAttachedShortValue = shortFlagIndex !== -1 && shortFlagIndex < token.length - 1; |
| if (token.includes("=") || hasAttachedShortValue || (!option.required && !option.optional)) { |
| return 1; |
| } |
| const next = argv[index + 1]; |
| if (option.required) { |
| return next === undefined ? 0 : 2; |
| } |
| const optionalValue = next && (!next.startsWith("-") || /^-\d/.test(next)); |
| return optionalValue ? 2 : 1; |
| } |
|
|
| function findCommandPathEnd(argv: readonly string[], command: Command): number { |
| const path = getCommandPathFromRoot(command); |
| const root = path[0]?.parent; |
| if (!root) { |
| return -1; |
| } |
| const selectedCommands = [root]; |
| let pathIndex = 0; |
| for (let index = 2; index < argv.length; index += 1) { |
| const token = argv[index] ?? ""; |
| const option = findNearestOption(selectedCommands, token); |
| if (option) { |
| const count = optionTokenCount(option, argv, index); |
| if (count === 0) { |
| return -1; |
| } |
| index += count - 1; |
| continue; |
| } |
| const nextCommand = path[pathIndex]; |
| if (!nextCommand || !matchesCommandName(nextCommand, token)) { |
| return -1; |
| } |
| selectedCommands.push(nextCommand); |
| pathIndex += 1; |
| if (pathIndex === path.length) { |
| return index + 1; |
| } |
| } |
| return -1; |
| } |
|
|
| |
| function hoistLazyParentOptions( |
| argv: string[], |
| parentCommand: Command, |
| lazyCommandName: string, |
| ): string[] { |
| let lazyCommandIndex = findCommandPathEnd(argv, parentCommand); |
| if (lazyCommandIndex === -1) { |
| return argv; |
| } |
| while (lazyCommandIndex < argv.length) { |
| const option = findOption(parentCommand, argv[lazyCommandIndex] ?? ""); |
| if (!option) { |
| break; |
| } |
| const count = optionTokenCount(option, argv, lazyCommandIndex); |
| if (count === 0) { |
| return argv; |
| } |
| lazyCommandIndex += count; |
| } |
| if (argv[lazyCommandIndex] !== lazyCommandName) { |
| return argv; |
| } |
|
|
| const lazyCommand = parentCommand.commands.find((command) => |
| matchesCommandName(command, lazyCommandName), |
| ); |
| if (!lazyCommand) { |
| return argv; |
| } |
| let selectedCommand = lazyCommand; |
| const selectedCommands = [selectedCommand]; |
|
|
| const hoisted: string[] = []; |
| const remaining: string[] = []; |
| let acceptsSubcommands = true; |
| for (let index = lazyCommandIndex + 1; index < argv.length; index += 1) { |
| const token = argv[index] ?? ""; |
| if (token === "--") { |
| remaining.push(...argv.slice(index)); |
| break; |
| } |
| const childOption = findNearestOption(selectedCommands, token); |
| const parentOption = findOption(parentCommand, token); |
| const option = childOption ?? parentOption; |
| if (option) { |
| const count = optionTokenCount(option, argv, index); |
| if (count === 0) { |
| return argv; |
| } |
| const tokens = argv.slice(index, index + count); |
| (childOption ? remaining : hoisted).push(...tokens); |
| index += count - 1; |
| continue; |
| } |
| if (acceptsSubcommands && !token.startsWith("-")) { |
| const nextCommand: Command | undefined = selectedCommand.commands.find((command) => |
| matchesCommandName(command, token), |
| ); |
| if (nextCommand) { |
| selectedCommand = nextCommand; |
| selectedCommands.push(nextCommand); |
| } else { |
| acceptsSubcommands = false; |
| } |
| } |
| remaining.push(token); |
| } |
| return hoisted.length === 0 |
| ? argv |
| : [...argv.slice(0, lazyCommandIndex), ...hoisted, lazyCommandName, ...remaining]; |
| } |
|
|
| |
| export async function reparseProgramFromActionCommand( |
| program: Command, |
| actionCommand: Command, |
| ): Promise<void> { |
| const rootProgram = findRootCommand(actionCommand) as Command & { rawArgs: string[] }; |
| |
| const parseArgv = buildParseArgv(rootProgram.rawArgs, rootProgram.name()); |
| const normalizedArgv = hoistLazyParentOptions(parseArgv, program, actionCommand.name()); |
| await rootProgram.parseAsync(normalizedArgv); |
| } |
|
|