diff --git a/src/index.ts b/src/index.ts
index 29ae477..5486f4c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -17,8 +17,8 @@ export type { RuleContext } from './core/rule';
export { invokeRuleContext, createRule } from './core/rule';
// Utils
-export type { Command } from './utils/command';
-export { parseCommand } from './utils/command';
+export type { Command, CommandSchema, CommandParamSchema, CommandOptionSchema, CommandFlagSchema } from './utils/command';
+export { parseCommand, parseCommandSchema, validateCommand } from './utils/command';
export type { Entity, EntityAccessor } from './utils/entity';
export { createEntityCollection } from './utils/entity';
diff --git a/src/utils/command.ts b/src/utils/command.ts
index 66e5d1a..05942e7 100644
--- a/src/utils/command.ts
+++ b/src/utils/command.ts
@@ -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...] 可选可变参数
+ * - --flag 长格式标志
+ * - -f 短格式标志
+ * - --option 长格式选项
+ * - -o 短格式选项
+ *
+ * @example
+ * parseCommandSchema('move [to...] [--force] [-f] [--speed ]')
+ */
+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 )
+ 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 是 格式,则是选项
+ 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 是 格式,则是选项
+ 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])
+ */
+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 === ']') {
+ // 结束括号,将内容加上括号作为一个 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 };
+}
diff --git a/tests/utils/command-schema.test.ts b/tests/utils/command-schema.test.ts
new file mode 100644
index 0000000..6592cc9
--- /dev/null
+++ b/tests/utils/command-schema.test.ts
@@ -0,0 +1,238 @@
+import { describe, it, expect } from 'vitest';
+import { parseCommand, parseCommandSchema, validateCommand } from '../../src/utils/command';
+
+describe('parseCommandSchema', () => {
+ it('should parse empty schema', () => {
+ const schema = parseCommandSchema('');
+ expect(schema).toEqual({
+ name: '',
+ params: [],
+ options: [],
+ flags: [],
+ });
+ });
+
+ it('should parse command name only', () => {
+ const schema = parseCommandSchema('move');
+ expect(schema).toEqual({
+ name: 'move',
+ params: [],
+ options: [],
+ flags: [],
+ });
+ });
+
+ it('should parse required params', () => {
+ const schema = parseCommandSchema('move ');
+ expect(schema.params).toEqual([
+ { name: 'from', required: true, variadic: false },
+ { name: 'to', required: true, variadic: false },
+ ]);
+ });
+
+ it('should parse optional params', () => {
+ const schema = parseCommandSchema('move [to]');
+ expect(schema.params).toEqual([
+ { name: 'from', required: true, variadic: false },
+ { name: 'to', required: false, variadic: false },
+ ]);
+ });
+
+ it('should parse variadic params', () => {
+ const schema = parseCommandSchema('move [targets...]');
+ expect(schema.params).toEqual([
+ { name: 'from', required: true, variadic: false },
+ { name: 'targets', required: false, variadic: true },
+ ]);
+ });
+
+ it('should parse required variadic params', () => {
+ const schema = parseCommandSchema('move ');
+ expect(schema.params).toEqual([
+ { name: 'targets', required: true, variadic: true },
+ ]);
+ });
+
+ it('should parse long flags', () => {
+ const schema = parseCommandSchema('move [--force] [--quiet]');
+ expect(schema.flags).toEqual([
+ { name: 'force' },
+ { name: 'quiet' },
+ ]);
+ });
+
+ it('should parse short flags', () => {
+ const schema = parseCommandSchema('move [-f] [-q]');
+ expect(schema.flags).toEqual([
+ { name: 'f', short: 'f' },
+ { name: 'q', short: 'q' },
+ ]);
+ });
+
+ it('should parse long options', () => {
+ const schema = parseCommandSchema('move --x [--y value]');
+ expect(schema.options).toEqual([
+ { name: 'x', required: true },
+ { name: 'y', required: false },
+ ]);
+ });
+
+ it('should parse short options', () => {
+ const schema = parseCommandSchema('move -x [-y value]');
+ expect(schema.options).toEqual([
+ { name: 'x', short: 'x', required: true },
+ { name: 'y', short: 'y', required: false },
+ ]);
+ });
+
+ it('should parse mixed schema', () => {
+ const schema = parseCommandSchema('move [--force] [-f] [--speed ] [-s val]');
+ expect(schema).toEqual({
+ name: 'move',
+ params: [
+ { name: 'from', required: true, variadic: false },
+ { name: 'to', required: true, variadic: false },
+ ],
+ flags: [
+ { name: 'force' },
+ { name: 'f', short: 'f' },
+ ],
+ options: [
+ { name: 'speed', required: false },
+ { name: 's', short: 's', required: false },
+ ],
+ });
+ });
+
+ it('should handle complex schema', () => {
+ const schema = parseCommandSchema('place [x...] [--rotate ] [--force] [-f]');
+ expect(schema.name).toBe('place');
+ expect(schema.params).toHaveLength(3);
+ expect(schema.flags).toHaveLength(2);
+ expect(schema.options).toHaveLength(1);
+ });
+});
+
+describe('validateCommand', () => {
+ it('should validate correct command', () => {
+ const schema = parseCommandSchema('move ');
+ const command = parseCommand('move meeple1 region1');
+ const result = validateCommand(command, schema);
+ expect(result).toEqual({ valid: true });
+ });
+
+ it('should reject wrong command name', () => {
+ const schema = parseCommandSchema('move ');
+ const command = parseCommand('place meeple1');
+ const result = validateCommand(command, schema);
+ expect(result).toEqual({
+ valid: false,
+ errors: expect.arrayContaining([
+ expect.stringContaining('命令名称不匹配'),
+ ]),
+ });
+ });
+
+ it('should reject missing required params', () => {
+ const schema = parseCommandSchema('move ');
+ const command = parseCommand('move meeple1');
+ const result = validateCommand(command, schema);
+ expect(result).toEqual({
+ valid: false,
+ errors: expect.arrayContaining([
+ expect.stringContaining('参数不足'),
+ ]),
+ });
+ });
+
+ it('should accept optional params missing', () => {
+ const schema = parseCommandSchema('move [to]');
+ const command = parseCommand('move meeple1');
+ const result = validateCommand(command, schema);
+ expect(result).toEqual({ valid: true });
+ });
+
+ it('should reject extra params without variadic', () => {
+ const schema = parseCommandSchema('move ');
+ const command = parseCommand('move meeple1 region1 extra');
+ const result = validateCommand(command, schema);
+ expect(result).toEqual({
+ valid: false,
+ errors: expect.arrayContaining([
+ expect.stringContaining('参数过多'),
+ ]),
+ });
+ });
+
+ it('should accept extra params with variadic', () => {
+ const schema = parseCommandSchema('move [targets...]');
+ const command = parseCommand('move meeple1 region1 region2 region3');
+ const result = validateCommand(command, schema);
+ expect(result).toEqual({ valid: true });
+ });
+
+ it('should reject missing required option', () => {
+ const schema = parseCommandSchema('move --speed ');
+ const command = parseCommand('move meeple1');
+ const result = validateCommand(command, schema);
+ expect(result).toEqual({
+ valid: false,
+ errors: expect.arrayContaining([
+ expect.stringContaining('缺少必需选项'),
+ ]),
+ });
+ });
+
+ it('should accept present required option', () => {
+ const schema = parseCommandSchema('move --speed ');
+ const command = parseCommand('move meeple1 --speed 10');
+ const result = validateCommand(command, schema);
+ expect(result).toEqual({ valid: true });
+ });
+
+ it('should accept optional option missing', () => {
+ const schema = parseCommandSchema('move [--speed [val]]');
+ const command = parseCommand('move meeple1');
+ const result = validateCommand(command, schema);
+ expect(result).toEqual({ valid: true });
+ });
+
+ it('should accept flags present or not', () => {
+ const schema = parseCommandSchema('move [--force]');
+ const cmd1 = parseCommand('move meeple1');
+ const cmd2 = parseCommand('move meeple1 --force');
+ expect(validateCommand(cmd1, schema)).toEqual({ valid: true });
+ expect(validateCommand(cmd2, schema)).toEqual({ valid: true });
+ });
+
+ it('should validate short form option', () => {
+ const schema = parseCommandSchema('move -s ');
+ const command = parseCommand('move meeple1 -s 10');
+ const result = validateCommand(command, schema);
+ expect(result).toEqual({ valid: true });
+ });
+
+ it('should provide detailed error messages', () => {
+ const schema = parseCommandSchema('place --rotate ');
+ const command = parseCommand('place meeple1');
+ const result = validateCommand(command, schema);
+ expect(result.valid).toBe(false);
+ if (!result.valid) {
+ expect(result.errors.length).toBeGreaterThanOrEqual(1);
+ }
+ });
+});
+
+describe('integration', () => {
+ it('should work together parse and validate', () => {
+ const schemaStr = 'place [--x ] [--y [val]] [--force] [-f]';
+ const schema = parseCommandSchema(schemaStr);
+
+ const validCmd = parseCommand('place meeple1 board --x 5 --force');
+ expect(validateCommand(validCmd, schema)).toEqual({ valid: true });
+
+ const invalidCmd = parseCommand('place meeple1');
+ const result = validateCommand(invalidCmd, schema);
+ expect(result.valid).toBe(false);
+ });
+});