From 4761806a02afecda0e630909e985f1b37139b155 Mon Sep 17 00:00:00 2001 From: hypercross Date: Wed, 1 Apr 2026 22:37:15 +0800 Subject: [PATCH] refactor: break up command.ts --- src/utils/command.ts | 672 +------------------------- src/utils/command/command-parse.ts | 119 +++++ src/utils/command/command-validate.ts | 103 ++++ src/utils/command/schema-parse.ts | 264 ++++++++++ src/utils/command/types.ts | 43 ++ 5 files changed, 539 insertions(+), 662 deletions(-) create mode 100644 src/utils/command/command-parse.ts create mode 100644 src/utils/command/command-validate.ts create mode 100644 src/utils/command/schema-parse.ts create mode 100644 src/utils/command/types.ts diff --git a/src/utils/command.ts b/src/utils/command.ts index 731123e..2bc769e 100644 --- a/src/utils/command.ts +++ b/src/utils/command.ts @@ -1,662 +1,10 @@ -import { defineSchema, type ParsedSchema, ParseError } from 'inline-schema'; - -export type Command = { - name: string; - flags: Record; - options: Record; - params: unknown[]; -} - -/** - * 命令参数 schema 定义 - */ -export type CommandParamSchema = { - /** 参数名称 */ - name: string; - /** 是否必需 */ - required: boolean; - /** 是否可变参数(可以接收多个值) */ - variadic: boolean; - /** 参数类型 schema(用于解析和验证) */ - schema?: ParsedSchema; -} - -/** - * 命令选项 schema 定义 - */ -export type CommandOptionSchema = { - /** 选项名称(长格式,不含 --) */ - name: string; - /** 短格式名称(不含 -) */ - short?: string; - /** 是否必需 */ - required: boolean; - /** 默认值 */ - defaultValue?: unknown; - /** 选项类型 schema(用于解析和验证) */ - schema?: ParsedSchema; -} - -/** - * 命令标志 schema 定义 - */ -export type CommandFlagSchema = { - /** 标志名称(长格式,不含 --) */ - name: string; - /** 短格式名称(不含 -) */ - short?: string; -} - -/** - * 命令完整 schema 定义 - */ -export type CommandSchema = { - /** 命令名称 */ - name: string; - /** 参数定义列表 */ - params: CommandParamSchema[]; - /** 选项定义列表 */ - options: CommandOptionSchema[]; - /** 标志定义列表 */ - flags: CommandFlagSchema[]; -} - -/** - * 解析命令行输入字符串为 Command 对象 - * 支持格式:commandName [params...] [--flags...] [-o value...] - * 支持引号:单引号 (') 和双引号 (") 可以包裹包含空格的参数 - * 支持转义:使用反斜杠 (\) 转义引号或反斜杠本身 - * - * @example - * parseCommand("move meeple1 region1 --force -x 10") - * // returns { name: "move", params: ["meeple1", "region1"], flags: { force: true }, options: { x: "10" } } - * parseCommand('place tile "large castle" --x 5') - * // returns { name: "place", params: ["tile", "large castle"], flags: {}, options: { x: "5" } } - */ -export function parseCommand(input: string): Command { - const tokens = tokenize(input); - - if (tokens.length === 0) { - return { name: '', flags: {}, options: {}, params: [] }; - } - - const name = tokens[0]; - const params: unknown[] = []; - const flags: Record = {}; - const options: Record = {}; - - let i = 1; - while (i < tokens.length) { - const token = tokens[i]; - - if (token.startsWith('--') && !/^-?\d+$/.test(token)) { - // 长格式标志或选项:--flag 或 --option value - const key = token.slice(2); - const nextToken = tokens[i + 1]; - - // 如果下一个 token 存在且不以 - 开头(或者是负数),则是选项值 - if (nextToken && (!nextToken.startsWith('-') || /^-\d+$/.test(nextToken))) { - options[key] = nextToken; - i += 2; - } else { - // 否则是布尔标志 - flags[key] = true; - i++; - } - } else if (token.startsWith('-') && token.length > 1 && !/^-?\d+$/.test(token)) { - // 短格式标志或选项:-f 或 -o value(但不匹配负数) - const key = token.slice(1); - const nextToken = tokens[i + 1]; - - // 如果下一个 token 存在且不以 - 开头(或者是负数),则是选项值 - if (nextToken && (!nextToken.startsWith('-') || /^-\d+$/.test(nextToken))) { - options[key] = nextToken; - i += 2; - } else { - // 否则是布尔标志 - flags[key] = true; - i++; - } - } else { - // 普通参数(包括负数) - params.push(token); - i++; - } - } - - return { name, flags, options, params }; -} - -/** - * 将输入字符串分解为 tokens,支持引号和转义 - */ -function tokenize(input: string): string[] { - const tokens: string[] = []; - let current = ''; - let inQuote: string | null = null; - let inBracket = false; - let bracketDepth = 0; - let escaped = false; - let i = 0; - - while (i < input.length) { - const char = input[i]; - - if (escaped) { - current += char; - escaped = false; - } else if (char === '\\') { - escaped = true; - } else if (inQuote) { - if (char === inQuote) { - inQuote = null; - } else { - current += char; - } - } else if (char === '"' || char === "'") { - inQuote = char; - } else if (char === '[') { - if (inBracket) { - bracketDepth++; - current += char; - } else { - if (current.length > 0) { - tokens.push(current); - current = ''; - } - inBracket = true; - bracketDepth = 1; - current = '['; - } - } else if (char === ']') { - if (inBracket) { - bracketDepth--; - current += char; - if (bracketDepth === 0) { - tokens.push(current); - current = ''; - inBracket = false; - } - } else { - current += char; - } - } else if (/\s/.test(char)) { - if (inBracket) { - current += char; - } else if (current.length > 0) { - tokens.push(current); - current = ''; - } - } else { - current += char; - } - - i++; - } - - if (current.length > 0) { - tokens.push(current); - } - - return tokens; -} - -/** - * 解析命令 schema 字符串为 CommandSchema 对象 - * 支持语法: - * - 必需参数 - * - [param] 可选参数 - * - 必需可变参数 - * - [param...] 可选可变参数 - * - 带类型定义的必需参数 - * - [param: type] 带类型定义的可选参数 - * - --flag 长格式标志(布尔类型) - * - --flag: boolean 长格式标志(布尔类型,与上面等价) - * - -f 短格式标志 - * - --option: type 带类型的长格式选项 - * - --option: type = default 带默认值的选项 - * - --option: type -o 带短别名的选项 - * - --option: type -o = default 带短别名和默认值的选项 - * - -o: type 带类型的短格式选项 - * - * 类型语法使用 inline-schema 格式(使用 ; 而非 ,): - * - string, number, boolean - * - [string; number] 元组 - * - string[] 数组 - * - [string; number][] 元组数组 - * - * @example - * parseCommandSchema('move [to...] [--force] [-f] [--speed: number]') - * parseCommandSchema('move [--all]') - * parseCommandSchema('move [--speed: number = 10 -s]') - */ -export function parseCommandSchema(schemaStr: string, name?: string): CommandSchema { - const schema: CommandSchema = { - name: name ?? '', - params: [], - options: [], - flags: [], - }; - - const tokens = tokenizeSchema(schemaStr); - if (tokens.length === 0) { - return schema; - } - - const startIdx = name !== undefined ? 0 : 1; - schema.name = name ?? tokens[0]; - - let i = startIdx; - while (i < tokens.length) { - const token = tokens[i]; - - if (token.startsWith('[') && token.endsWith(']')) { - const inner = token.slice(1, -1).trim(); - - if (inner.startsWith('--')) { - const result = parseOptionToken(inner.slice(2), false); - if (result.isFlag) { - schema.flags.push({ name: result.name, short: result.short }); - } else { - schema.options.push({ - name: result.name, - short: result.short, - required: false, - defaultValue: result.defaultValue, - schema: result.schema, - }); - } - } else if (inner.startsWith('-') && inner.length > 1 && !inner.includes('--')) { - const result = parseOptionToken(inner.slice(1), false); - if (result.isFlag) { - schema.flags.push({ name: result.name, short: result.short || result.name }); - } else { - schema.options.push({ - name: result.name, - short: result.short || result.name, - required: false, - defaultValue: result.defaultValue, - schema: result.schema, - }); - } - } else { - const isVariadic = inner.endsWith('...'); - let paramContent = isVariadic ? inner.slice(0, -3) : inner; - let parsedSchema: ParsedSchema | undefined; - - if (paramContent.includes(':')) { - const colonIndex = paramContent.indexOf(':'); - const name = paramContent.slice(0, colonIndex).trim(); - const typeStr = paramContent.slice(colonIndex + 1).trim(); - try { - parsedSchema = defineSchema(typeStr); - } catch (e) { - // 不是有效的 schema - } - paramContent = name; - } - - schema.params.push({ - name: paramContent, - required: false, - variadic: isVariadic, - schema: parsedSchema, - }); - } - i++; - } else if (token.startsWith('--')) { - const result = parseOptionToken(token.slice(2), true); - if (result.isFlag) { - schema.flags.push({ name: result.name, short: result.short }); - } else { - schema.options.push({ - name: result.name, - short: result.short, - required: true, - defaultValue: result.defaultValue, - schema: result.schema, - }); - } - i++; - } else if (token.startsWith('-') && token.length > 1 && !/^-?\d+$/.test(token)) { - const result = parseOptionToken(token.slice(1), true); - if (result.isFlag) { - schema.flags.push({ name: result.name, short: result.short || result.name }); - } else { - schema.options.push({ - name: result.name, - short: result.short || result.name, - required: true, - defaultValue: result.defaultValue, - schema: result.schema, - }); - } - i++; - } else if (token.startsWith('<') && token.endsWith('>')) { - const isVariadic = token.endsWith('...>'); - let paramContent = token.replace(/^<+|>+$/g, ''); - if (isVariadic) { - paramContent = paramContent.replace(/\.\.\.$/, ''); - } - let parsedSchema: ParsedSchema | undefined; - - if (paramContent.includes(':')) { - const colonIndex = paramContent.indexOf(':'); - const name = paramContent.slice(0, colonIndex).trim(); - const typeStr = paramContent.slice(colonIndex + 1).trim(); - try { - parsedSchema = defineSchema(typeStr); - } catch (e) { - // 不是有效的 schema - } - paramContent = name; - } - - schema.params.push({ - name: paramContent, - required: true, - variadic: isVariadic, - schema: parsedSchema, - }); - i++; - } else { - i++; - } - } - - return schema; -} - -/** - * 解析选项/标志 token 的结果 - */ -interface ParsedOptionResult { - name: string; - short?: string; - isFlag: boolean; - schema?: ParsedSchema; - defaultValue?: unknown; -} - -/** - * 解析单个选项/标志 token - * 支持格式: - * - flag → 标志 - * - flag: boolean → 标志(统一处理) - * - option: type → 选项 - * - option: type -s → 选项带短别名 - * - option: type = default → 选项带默认值 - * - option: type -s = default → 选项带短别名和默认值 - */ -function parseOptionToken(token: string, required: boolean): ParsedOptionResult { - const parts = token.split(/\s+/); - const mainPart = parts[0]; - - let name: string; - let typeStr: string | undefined; - let isFlag = false; - - if (mainPart.endsWith(':')) { - name = mainPart.slice(0, -1).trim(); - typeStr = parts[1] || 'string'; - } else if (mainPart.includes(':')) { - const [optName, optType] = mainPart.split(':').map(s => s.trim()); - name = optName; - typeStr = optType; - } else { - name = mainPart; - isFlag = true; - } - - if (typeStr === 'boolean') { - isFlag = true; - typeStr = undefined; - } - - let short: string | undefined; - let defaultValue: unknown; - let schema: ParsedSchema | undefined; - - for (let i = 1; i < parts.length; i++) { - const part = parts[i]; - - if (part.startsWith('-') && part.length === 2) { - short = part.slice(1); - } else if (part === '=') { - const valuePart = parts[i + 1]; - if (valuePart) { - try { - defaultValue = JSON.parse(valuePart); - } catch { - defaultValue = valuePart; - } - i++; - } - } else if (part.startsWith('=')) { - const valuePart = part.slice(1); - try { - defaultValue = JSON.parse(valuePart); - } catch { - defaultValue = valuePart; - } - } - } - - if (typeStr && !isFlag) { - try { - schema = defineSchema(typeStr); - } catch { - // 不是有效的 schema - } - } - - return { name, short, isFlag, schema, defaultValue }; -} - -/** - * 检查 token 是否是值占位符(如 或 [value]) - */ -function isValuePlaceholder(token: string): boolean { - return (token.startsWith('<') && token.endsWith('>')) || - (token.startsWith('[') && token.endsWith(']')); -} - -/** - * 检查 token 是否是参数占位符 - */ -function isParamPlaceholder(token: string): boolean { - // 参数占位符必须以 < 或 [ 开头 - if (!token.startsWith('<') && !token.startsWith('[')) { - return false; - } - // 检查是否是选项的值占位符(如 <--opt 中的 ) - // 这种情况应该由选项处理逻辑处理,不作为独立参数 - return true; -} - -/** - * 将 schema 字符串分解为 tokens - * 支持方括号分组:[...args] [--flag] 等 - * 支持尖括号分组: 等 - */ -function tokenizeSchema(input: string): string[] { - const tokens: string[] = []; - let current = ''; - let inBracket = false; - let bracketContent = ''; - let i = 0; - - while (i < input.length) { - const char = input[i]; - - if (inBracket) { - if (char === ']') { - tokens.push(`[${bracketContent}]`); - inBracket = false; - bracketContent = ''; - current = ''; - } else if (char === '[') { - bracketContent += char; - } else { - bracketContent += char; - } - } else if (/\s/.test(char)) { - if (current.length > 0) { - tokens.push(current); - current = ''; - } - } else if (char === '[') { - if (current.length > 0) { - tokens.push(current); - current = ''; - } - inBracket = true; - bracketContent = ''; - } else if (char === '<') { - let angleContent = '<'; - i++; - while (i < input.length && input[i] !== '>') { - angleContent += input[i]; - i++; - } - angleContent += '>'; - tokens.push(angleContent); - } else { - current += char; - } - - i++; - } - - if (current.length > 0) { - tokens.push(current); - } - - if (bracketContent.length > 0) { - tokens.push(`[${bracketContent}`); - } - - return tokens; -} - -/** - * 根据 schema 验证命令 - * @returns 验证结果,valid 为 true 表示通过,否则包含错误信息 - */ -export function validateCommand( - command: Command, - schema: CommandSchema -): { valid: true } | { valid: false; errors: string[] } { - const errors = validateCommandCore(command, schema); - - if (errors.length > 0) { - return { valid: false, errors }; - } - - return { valid: true }; -} - -/** - * 核心验证逻辑,返回错误数组 - */ -function validateCommandCore(command: Command, schema: CommandSchema): string[] { - const errors: string[] = []; - - if (command.name !== schema.name) { - errors.push(`命令名称不匹配:期望 "${schema.name}",实际 "${command.name}"`); - } - - const requiredParams = schema.params.filter(p => p.required); - const variadicParam = schema.params.find(p => p.variadic); - - if (command.params.length < requiredParams.length) { - errors.push(`参数不足:至少需要 ${requiredParams.length} 个参数,实际 ${command.params.length} 个`); - } - - if (!variadicParam && command.params.length > schema.params.length) { - errors.push(`参数过多:最多 ${schema.params.length} 个参数,实际 ${command.params.length} 个`); - } - - const requiredOptions = schema.options.filter(o => o.required); - for (const opt of requiredOptions) { - const hasOption = opt.name in command.options || (opt.short && opt.short in command.options); - if (!hasOption) { - errors.push(`缺少必需选项:--${opt.name}${opt.short ? ` 或 -${opt.short}` : ''}`); - } - } - - return errors; -} - -/** - * 根据 schema 解析并验证命令,返回类型化的命令对象 - * 如果 schema 中定义了类型,会自动解析参数和选项的值 - * - * @param input 命令行输入字符串 - * @param schemaStr 命令 schema 字符串 - * @returns 解析后的命令对象和验证结果 - * - * @example - * const result = parseCommandWithSchema( - * 'move [1; 2] region1 --all true', - * 'move [--all: boolean]' - * ); - * // result.command.params[0] = ['1', '2'] (已解析为元组) - * // result.command.options.all = true (已解析为布尔值) - */ -export function parseCommandWithSchema( - input: string, - schemaStr: string -): { command: Command; valid: true } | { command: Command; valid: false; errors: string[] } { - const schema = parseCommandSchema(schemaStr); - const command = parseCommand(input); - - const errors = validateCommandCore(command, schema); - if (errors.length > 0) { - return { command, valid: false, errors }; - } - - const parseErrors: string[] = []; - - const parsedParams: unknown[] = []; - for (let i = 0; i < command.params.length; i++) { - const paramValue = command.params[i]; - const paramSchema = schema.params[i]?.schema; - - if (paramSchema) { - try { - const parsed = typeof paramValue === 'string' - ? paramSchema.parse(paramValue) - : paramValue; - parsedParams.push(parsed); - } catch (e) { - const err = e as ParseError; - parseErrors.push(`参数 "${schema.params[i]?.name}" 解析失败:${err.message}`); - } - } else { - parsedParams.push(paramValue); - } - } - - const parsedOptions: Record = { ...command.options }; - for (const [key, value] of Object.entries(command.options)) { - const optSchema = schema.options.find(o => o.name === key || o.short === key); - if (optSchema?.schema && typeof value === 'string') { - try { - parsedOptions[key] = optSchema.schema.parse(value); - } catch (e) { - const err = e as ParseError; - parseErrors.push(`选项 "--${key}" 解析失败:${err.message}`); - } - } - } - - if (parseErrors.length > 0) { - return { command: { ...command, params: parsedParams, options: parsedOptions }, valid: false, errors: parseErrors }; - } - - return { - command: { ...command, params: parsedParams, options: parsedOptions }, - valid: true, - }; -} +export { parseCommand } from './command/command-parse.js'; +export { parseCommandSchema } from './command/schema-parse.js'; +export { validateCommand, parseCommandWithSchema } from './command/command-validate.js'; +export type { + Command, + CommandParamSchema, + CommandOptionSchema, + CommandFlagSchema, + CommandSchema, +} from './command/types.js'; diff --git a/src/utils/command/command-parse.ts b/src/utils/command/command-parse.ts new file mode 100644 index 0000000..c3aa8b2 --- /dev/null +++ b/src/utils/command/command-parse.ts @@ -0,0 +1,119 @@ +import type { Command } from './types.js'; + +export function parseCommand(input: string): Command { + const tokens = tokenize(input); + + if (tokens.length === 0) { + return { name: '', flags: {}, options: {}, params: [] }; + } + + const name = tokens[0]; + const params: unknown[] = []; + const flags: Record = {}; + const options: Record = {}; + + let i = 1; + while (i < tokens.length) { + const token = tokens[i]; + + if (token.startsWith('--') && !/^-?\d+$/.test(token)) { + const key = token.slice(2); + const nextToken = tokens[i + 1]; + + if (nextToken && (!nextToken.startsWith('-') || /^-\d+$/.test(nextToken))) { + options[key] = nextToken; + i += 2; + } else { + flags[key] = true; + i++; + } + } else if (token.startsWith('-') && token.length > 1 && !/^-?\d+$/.test(token)) { + const key = token.slice(1); + const nextToken = tokens[i + 1]; + + if (nextToken && (!nextToken.startsWith('-') || /^-\d+$/.test(nextToken))) { + options[key] = nextToken; + i += 2; + } else { + flags[key] = true; + i++; + } + } else { + params.push(token); + i++; + } + } + + return { name, flags, options, params }; +} + +function tokenize(input: string): string[] { + const tokens: string[] = []; + let current = ''; + let inQuote: string | null = null; + let inBracket = false; + let bracketDepth = 0; + let escaped = false; + let i = 0; + + while (i < input.length) { + const char = input[i]; + + if (escaped) { + current += char; + escaped = false; + } else if (char === '\\') { + escaped = true; + } else if (inQuote) { + if (char === inQuote) { + inQuote = null; + } else { + current += char; + } + } else if (char === '"' || char === "'") { + inQuote = char; + } else if (char === '[') { + if (inBracket) { + bracketDepth++; + current += char; + } else { + if (current.length > 0) { + tokens.push(current); + current = ''; + } + inBracket = true; + bracketDepth = 1; + current = '['; + } + } else if (char === ']') { + if (inBracket) { + bracketDepth--; + current += char; + if (bracketDepth === 0) { + tokens.push(current); + current = ''; + inBracket = false; + } + } else { + current += char; + } + } else if (/\s/.test(char)) { + if (inBracket) { + current += char; + } else if (current.length > 0) { + tokens.push(current); + current = ''; + } + } else { + current += char; + } + + i++; + } + + if (current.length > 0) { + tokens.push(current); + } + + return tokens; +} diff --git a/src/utils/command/command-validate.ts b/src/utils/command/command-validate.ts new file mode 100644 index 0000000..6e4a6f8 --- /dev/null +++ b/src/utils/command/command-validate.ts @@ -0,0 +1,103 @@ +import { type ParseError } from 'inline-schema'; +import type { Command, CommandSchema } from './types.js'; +import { parseCommand } from './command-parse.js'; +import { parseCommandSchema } from './schema-parse.js'; + +export function validateCommand( + command: Command, + schema: CommandSchema +): { valid: true } | { valid: false; errors: string[] } { + const errors = validateCommandCore(command, schema); + + if (errors.length > 0) { + return { valid: false, errors }; + } + + return { valid: true }; +} + +function validateCommandCore(command: Command, schema: CommandSchema): string[] { + const errors: string[] = []; + + if (command.name !== schema.name) { + errors.push(`命令名称不匹配:期望 "${schema.name}",实际 "${command.name}"`); + } + + const requiredParams = schema.params.filter(p => p.required); + const variadicParam = schema.params.find(p => p.variadic); + + if (command.params.length < requiredParams.length) { + errors.push(`参数不足:至少需要 ${requiredParams.length} 个参数,实际 ${command.params.length} 个`); + } + + if (!variadicParam && command.params.length > schema.params.length) { + errors.push(`参数过多:最多 ${schema.params.length} 个参数,实际 ${command.params.length} 个`); + } + + const requiredOptions = schema.options.filter(o => o.required); + for (const opt of requiredOptions) { + const hasOption = opt.name in command.options || (opt.short && opt.short in command.options); + if (!hasOption) { + errors.push(`缺少必需选项:--${opt.name}${opt.short ? ` 或 -${opt.short}` : ''}`); + } + } + + return errors; +} + +export function parseCommandWithSchema( + input: string, + schemaStr: string +): { command: Command; valid: true } | { command: Command; valid: false; errors: string[] } { + const schema = parseCommandSchema(schemaStr); + const command = parseCommand(input); + + const errors = validateCommandCore(command, schema); + if (errors.length > 0) { + return { command, valid: false, errors }; + } + + const parseErrors: string[] = []; + + const parsedParams: unknown[] = []; + for (let i = 0; i < command.params.length; i++) { + const paramValue = command.params[i]; + const paramSchema = schema.params[i]?.schema; + + if (paramSchema) { + try { + const parsed = typeof paramValue === 'string' + ? paramSchema.parse(paramValue) + : paramValue; + parsedParams.push(parsed); + } catch (e) { + const err = e as ParseError; + parseErrors.push(`参数 "${schema.params[i]?.name}" 解析失败:${err.message}`); + } + } else { + parsedParams.push(paramValue); + } + } + + const parsedOptions: Record = { ...command.options }; + for (const [key, value] of Object.entries(command.options)) { + const optSchema = schema.options.find(o => o.name === key || o.short === key); + if (optSchema?.schema && typeof value === 'string') { + try { + parsedOptions[key] = optSchema.schema.parse(value); + } catch (e) { + const err = e as ParseError; + parseErrors.push(`选项 "--${key}" 解析失败:${err.message}`); + } + } + } + + if (parseErrors.length > 0) { + return { command: { ...command, params: parsedParams, options: parsedOptions }, valid: false, errors: parseErrors }; + } + + return { + command: { ...command, params: parsedParams, options: parsedOptions }, + valid: true, + }; +} diff --git a/src/utils/command/schema-parse.ts b/src/utils/command/schema-parse.ts new file mode 100644 index 0000000..1eec9d3 --- /dev/null +++ b/src/utils/command/schema-parse.ts @@ -0,0 +1,264 @@ +import { defineSchema, type ParsedSchema } from 'inline-schema'; +import type { CommandSchema, CommandParamSchema, CommandOptionSchema, CommandFlagSchema, ParsedOptionResult } from './types.js'; + +export function parseCommandSchema(schemaStr: string, name?: string): CommandSchema { + const schema: CommandSchema = { + name: name ?? '', + params: [], + options: [], + flags: [], + }; + + const tokens = tokenizeSchema(schemaStr); + if (tokens.length === 0) { + return schema; + } + + const startIdx = name !== undefined ? 0 : 1; + schema.name = name ?? tokens[0]; + + let i = startIdx; + while (i < tokens.length) { + const token = tokens[i]; + + if (token.startsWith('[') && token.endsWith(']')) { + const inner = token.slice(1, -1).trim(); + + if (inner.startsWith('--')) { + const result = parseOptionToken(inner.slice(2), false); + if (result.isFlag) { + schema.flags.push({ name: result.name, short: result.short }); + } else { + schema.options.push({ + name: result.name, + short: result.short, + required: false, + defaultValue: result.defaultValue, + schema: result.schema, + }); + } + } else if (inner.startsWith('-') && inner.length > 1 && !inner.includes('--')) { + const result = parseOptionToken(inner.slice(1), false); + if (result.isFlag) { + schema.flags.push({ name: result.name, short: result.short || result.name }); + } else { + schema.options.push({ + name: result.name, + short: result.short || result.name, + required: false, + defaultValue: result.defaultValue, + schema: result.schema, + }); + } + } else { + const isVariadic = inner.endsWith('...'); + let paramContent = isVariadic ? inner.slice(0, -3) : inner; + let parsedSchema: ParsedSchema | undefined; + + if (paramContent.includes(':')) { + const colonIdx = paramContent.indexOf(':'); + const name = paramContent.slice(0, colonIdx).trim(); + const typeStr = paramContent.slice(colonIdx + 1).trim(); + try { + parsedSchema = defineSchema(typeStr); + } catch (e) { + // 不是有效的 schema + } + paramContent = name; + } + + schema.params.push({ + name: paramContent, + required: false, + variadic: isVariadic, + schema: parsedSchema, + }); + } + i++; + } else if (token.startsWith('--')) { + const result = parseOptionToken(token.slice(2), true); + if (result.isFlag) { + schema.flags.push({ name: result.name, short: result.short }); + } else { + schema.options.push({ + name: result.name, + short: result.short, + required: true, + defaultValue: result.defaultValue, + schema: result.schema, + }); + } + i++; + } else if (token.startsWith('-') && token.length > 1 && !/^-?\d+$/.test(token)) { + const result = parseOptionToken(token.slice(1), true); + if (result.isFlag) { + schema.flags.push({ name: result.name, short: result.short || result.name }); + } else { + schema.options.push({ + name: result.name, + short: result.short || result.name, + required: true, + defaultValue: result.defaultValue, + schema: result.schema, + }); + } + i++; + } else if (token.startsWith('<') && token.endsWith('>')) { + const isVariadic = token.endsWith('...>'); + let paramContent = token.replace(/^<+|>+$/g, ''); + if (isVariadic) { + paramContent = paramContent.replace(/\.\.\.$/, ''); + } + let parsedSchema: ParsedSchema | undefined; + + if (paramContent.includes(':')) { + const colonIdx = paramContent.indexOf(':'); + const name = paramContent.slice(0, colonIdx).trim(); + const typeStr = paramContent.slice(colonIdx + 1).trim(); + try { + parsedSchema = defineSchema(typeStr); + } catch (e) { + // 不是有效的 schema + } + paramContent = name; + } + + schema.params.push({ + name: paramContent, + required: true, + variadic: isVariadic, + schema: parsedSchema, + }); + i++; + } else { + i++; + } + } + + return schema; +} + +function parseOptionToken(token: string, required: boolean): ParsedOptionResult { + const parts = token.split(/\s+/); + const mainPart = parts[0]; + + let name: string; + let typeStr: string | undefined; + let isFlag = false; + + if (mainPart.endsWith(':')) { + name = mainPart.slice(0, -1).trim(); + typeStr = parts[1] || 'string'; + } else if (mainPart.includes(':')) { + const [optName, optType] = mainPart.split(':').map(s => s.trim()); + name = optName; + typeStr = optType; + } else { + name = mainPart; + isFlag = true; + } + + if (typeStr === 'boolean') { + isFlag = true; + typeStr = undefined; + } + + let short: string | undefined; + let defaultValue: unknown; + let schema: ParsedSchema | undefined; + + for (let i = 1; i < parts.length; i++) { + const part = parts[i]; + + if (part.startsWith('-') && part.length === 2) { + short = part.slice(1); + } else if (part === '=') { + const valuePart = parts[i + 1]; + if (valuePart) { + try { + defaultValue = JSON.parse(valuePart); + } catch { + defaultValue = valuePart; + } + i++; + } + } else if (part.startsWith('=')) { + const valuePart = part.slice(1); + try { + defaultValue = JSON.parse(valuePart); + } catch { + defaultValue = valuePart; + } + } + } + + if (typeStr && !isFlag) { + try { + schema = defineSchema(typeStr); + } catch { + // 不是有效的 schema + } + } + + return { name, short, isFlag, schema, defaultValue }; +} + +function tokenizeSchema(input: string): string[] { + const tokens: string[] = []; + let current = ''; + let inBracket = false; + let bracketContent = ''; + let i = 0; + + while (i < input.length) { + const char = input[i]; + + if (inBracket) { + if (char === ']') { + tokens.push(`[${bracketContent}]`); + inBracket = false; + bracketContent = ''; + current = ''; + } else if (char === '[') { + bracketContent += char; + } else { + bracketContent += char; + } + } else if (/\s/.test(char)) { + if (current.length > 0) { + tokens.push(current); + current = ''; + } + } else if (char === '[') { + if (current.length > 0) { + tokens.push(current); + current = ''; + } + inBracket = true; + bracketContent = ''; + } else if (char === '<') { + let angleContent = '<'; + i++; + while (i < input.length && input[i] !== '>') { + angleContent += input[i]; + i++; + } + angleContent += '>'; + tokens.push(angleContent); + } else { + current += char; + } + + i++; + } + + if (current.length > 0) { + tokens.push(current); + } + + if (bracketContent.length > 0) { + tokens.push(`[${bracketContent}`); + } + + return tokens; +} diff --git a/src/utils/command/types.ts b/src/utils/command/types.ts new file mode 100644 index 0000000..d1c24f6 --- /dev/null +++ b/src/utils/command/types.ts @@ -0,0 +1,43 @@ +import { type ParsedSchema } from 'inline-schema'; + +export type Command = { + name: string; + flags: Record; + options: Record; + params: unknown[]; +} + +export type CommandParamSchema = { + name: string; + required: boolean; + variadic: boolean; + schema?: ParsedSchema; +} + +export type CommandOptionSchema = { + name: string; + short?: string; + required: boolean; + defaultValue?: unknown; + schema?: ParsedSchema; +} + +export type CommandFlagSchema = { + name: string; + short?: string; +} + +export type CommandSchema = { + name: string; + params: CommandParamSchema[]; + options: CommandOptionSchema[]; + flags: CommandFlagSchema[]; +} + +export interface ParsedOptionResult { + name: string; + short?: string; + isFlag: boolean; + schema?: ParsedSchema; + defaultValue?: unknown; +}