feat: add command schema

This commit is contained in:
hyper
2026-04-01 18:54:02 +08:00
parent 00bed92d46
commit 95015b090c
3 changed files with 567 additions and 2 deletions
+327
View File
@@ -5,6 +5,56 @@
params: string[];
}
/**
* 命令参数 schema 定义
*/
export type CommandParamSchema = {
/** 参数名称 */
name: string;
/** 是否必需 */
required: boolean;
/** 是否可变参数(可以接收多个值) */
variadic: boolean;
}
/**
* 命令选项 schema 定义
*/
export type CommandOptionSchema = {
/** 选项名称(长格式,不含 --) */
name: string;
/** 短格式名称(不含 - */
short?: string;
/** 是否必需 */
required: boolean;
/** 默认值 */
defaultValue?: string;
}
/**
* 命令标志 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...]
@@ -123,3 +173,280 @@ function tokenize(input: string): string[] {
return tokens;
}
/**
* 解析命令 schema 字符串为 CommandSchema 对象
* 支持语法:
* - <param> 必需参数
* - [param] 可选参数
* - <param...> 必需可变参数
* - [param...] 可选可变参数
* - --flag 长格式标志
* - -f 短格式标志
* - --option <value> 长格式选项
* - -o <value> 短格式选项
*
* @example
* parseCommandSchema('move <from> [to...] [--force] [-f] [--speed <val>]')
*/
export function parseCommandSchema(schemaStr: string): CommandSchema {
const schema: CommandSchema = {
name: '',
params: [],
options: [],
flags: [],
};
const tokens = tokenizeSchema(schemaStr);
if (tokens.length === 0) {
return schema;
}
// 第一个 token 是命令名称
schema.name = tokens[0];
let i = 1;
while (i < tokens.length) {
const token = tokens[i];
if (token.startsWith('[') && token.endsWith(']')) {
// 可选参数/标志/选项(方括号内的内容)
const inner = token.slice(1, -1).trim();
if (inner.startsWith('--')) {
// 可选长格式标志或选项
const parts = inner.split(/\s+/);
const name = parts[0].slice(2);
// 如果有额外的部分,则是选项(如 --opt value 或 --opt <value>
if (parts.length > 1) {
// 可选选项
schema.options.push({
name,
required: false,
});
} else {
// 可选标志
schema.flags.push({ name });
}
} else if (inner.startsWith('-') && inner.length > 1) {
// 可选短格式标志或选项
const parts = inner.split(/\s+/);
const short = parts[0].slice(1);
// 如果有额外的部分,则是选项
if (parts.length > 1) {
// 可选选项
schema.options.push({
name: short,
short,
required: false,
});
} else {
// 可选标志
schema.flags.push({ name: short, short });
}
} else {
// 可选参数
const isVariadic = inner.endsWith('...');
const name = isVariadic ? inner.slice(0, -3) : inner;
schema.params.push({
name,
required: false,
variadic: isVariadic,
});
}
i++;
} else if (token.startsWith('--')) {
// 长格式标志或选项(必需的,因为不在方括号内)
const name = token.slice(2);
const nextToken = tokens[i + 1];
// 如果下一个 token 是 <value> 格式,则是选项
if (nextToken && nextToken.startsWith('<') && nextToken.endsWith('>')) {
schema.options.push({
name,
required: true,
});
i += 2;
} else {
// 否则是标志
schema.flags.push({ name });
i++;
}
} else if (token.startsWith('-') && token.length > 1 && !/^-?\d+$/.test(token)) {
// 短格式标志或选项(必需的,因为不在方括号内)
const short = token.slice(1);
const nextToken = tokens[i + 1];
// 如果下一个 token 是 <value> 格式,则是选项
if (nextToken && nextToken.startsWith('<') && nextToken.endsWith('>')) {
schema.options.push({
name: short,
short,
required: true,
});
i += 2;
} else {
// 否则是标志
schema.flags.push({ name: short, short });
i++;
}
} else if (token.startsWith('<') && token.endsWith('>')) {
// 必需参数
const isVariadic = token.endsWith('...>');
const name = token.replace(/^[<]+|[>.>]+$/g, '');
schema.params.push({
name,
required: true,
variadic: isVariadic,
});
i++;
} else {
// 跳过无法识别的 token
i++;
}
}
return schema;
}
/**
* 检查 token 是否是值占位符(如 <value> 或 [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 <val> 中的 <val>
// 这种情况应该由选项处理逻辑处理,不作为独立参数
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 === ']') {
// 结束括号,将内容加上括号作为一个 token
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: 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}` : ''}`);
}
}
// 验证标志(标志都是可选的,除非未来扩展支持必需标志)
// 目前只检查是否有未定义的标志(可选的严格模式)
if (errors.length > 0) {
return { valid: false, errors };
}
return { valid: true };
}