Compare commits
2 Commits
c315e0643b
...
033fb6c894
| Author | SHA1 | Date |
|---|---|---|
|
|
033fb6c894 | |
|
|
d1ea04c442 |
|
|
@ -133,7 +133,9 @@ export function parseCommand(input: string): Command {
|
||||||
function tokenize(input: string): string[] {
|
function tokenize(input: string): string[] {
|
||||||
const tokens: string[] = [];
|
const tokens: string[] = [];
|
||||||
let current = '';
|
let current = '';
|
||||||
let inQuote: string | null = null; // ' 或 " 或 null
|
let inQuote: string | null = null;
|
||||||
|
let inBracket = false;
|
||||||
|
let bracketDepth = 0;
|
||||||
let escaped = false;
|
let escaped = false;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
|
|
@ -141,38 +143,57 @@ function tokenize(input: string): string[] {
|
||||||
const char = input[i];
|
const char = input[i];
|
||||||
|
|
||||||
if (escaped) {
|
if (escaped) {
|
||||||
// 转义字符:直接添加到当前 token
|
|
||||||
current += char;
|
current += char;
|
||||||
escaped = false;
|
escaped = false;
|
||||||
} else if (char === '\\') {
|
} else if (char === '\\') {
|
||||||
// 开始转义
|
|
||||||
escaped = true;
|
escaped = true;
|
||||||
} else if (inQuote) {
|
} else if (inQuote) {
|
||||||
// 在引号内
|
|
||||||
if (char === inQuote) {
|
if (char === inQuote) {
|
||||||
// 结束引号
|
|
||||||
inQuote = null;
|
inQuote = null;
|
||||||
} else {
|
} else {
|
||||||
current += char;
|
current += char;
|
||||||
}
|
}
|
||||||
} else if (char === '"' || char === "'") {
|
} else if (char === '"' || char === "'") {
|
||||||
// 开始引号
|
|
||||||
inQuote = 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)) {
|
} else if (/\s/.test(char)) {
|
||||||
// 空白字符
|
if (inBracket) {
|
||||||
if (current.length > 0) {
|
current += char;
|
||||||
|
} else if (current.length > 0) {
|
||||||
tokens.push(current);
|
tokens.push(current);
|
||||||
current = '';
|
current = '';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 普通字符
|
|
||||||
current += char;
|
current += char;
|
||||||
}
|
}
|
||||||
|
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理未闭合的引号
|
|
||||||
if (current.length > 0) {
|
if (current.length > 0) {
|
||||||
tokens.push(current);
|
tokens.push(current);
|
||||||
}
|
}
|
||||||
|
|
@ -189,11 +210,13 @@ function tokenize(input: string): string[] {
|
||||||
* - [param...] 可选可变参数
|
* - [param...] 可选可变参数
|
||||||
* - <param: type> 带类型定义的必需参数
|
* - <param: type> 带类型定义的必需参数
|
||||||
* - [param: type] 带类型定义的可选参数
|
* - [param: type] 带类型定义的可选参数
|
||||||
* - --flag 长格式标志
|
* - --flag 长格式标志(布尔类型)
|
||||||
|
* - --flag: boolean 长格式标志(布尔类型,与上面等价)
|
||||||
* - -f 短格式标志
|
* - -f 短格式标志
|
||||||
* - --option <value> 长格式选项
|
|
||||||
* - --option: type 带类型的长格式选项
|
* - --option: type 带类型的长格式选项
|
||||||
* - -o <value> 短格式选项
|
* - --option: type = default 带默认值的选项
|
||||||
|
* - --option: type -o 带短别名的选项
|
||||||
|
* - --option: type -o = default 带短别名和默认值的选项
|
||||||
* - -o: type 带类型的短格式选项
|
* - -o: type 带类型的短格式选项
|
||||||
*
|
*
|
||||||
* 类型语法使用 inline-schema 格式(使用 ; 而非 ,):
|
* 类型语法使用 inline-schema 格式(使用 ; 而非 ,):
|
||||||
|
|
@ -203,8 +226,9 @@ function tokenize(input: string): string[] {
|
||||||
* - [string; number][] 元组数组
|
* - [string; number][] 元组数组
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* parseCommandSchema('move <from> [to...] [--force] [-f] [--speed <val>]')
|
* parseCommandSchema('move <from> [to...] [--force] [-f] [--speed: number]')
|
||||||
* parseCommandSchema('move <from: [x: string; y: string]> <to: string> [--all: boolean]')
|
* parseCommandSchema('move <from: [x: string; y: string]> <to: string> [--all]')
|
||||||
|
* parseCommandSchema('move <from> <to> [--speed: number = 10 -s]')
|
||||||
*/
|
*/
|
||||||
export function parseCommandSchema(schemaStr: string): CommandSchema {
|
export function parseCommandSchema(schemaStr: string): CommandSchema {
|
||||||
const schema: CommandSchema = {
|
const schema: CommandSchema = {
|
||||||
|
|
@ -219,7 +243,6 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第一个 token 是命令名称
|
|
||||||
schema.name = tokens[0];
|
schema.name = tokens[0];
|
||||||
|
|
||||||
let i = 1;
|
let i = 1;
|
||||||
|
|
@ -227,92 +250,39 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
||||||
const token = tokens[i];
|
const token = tokens[i];
|
||||||
|
|
||||||
if (token.startsWith('[') && token.endsWith(']')) {
|
if (token.startsWith('[') && token.endsWith(']')) {
|
||||||
// 可选参数/标志/选项(方括号内的内容)
|
|
||||||
const inner = token.slice(1, -1).trim();
|
const inner = token.slice(1, -1).trim();
|
||||||
|
|
||||||
if (inner.startsWith('--')) {
|
if (inner.startsWith('--')) {
|
||||||
// 可选长格式标志或选项
|
const result = parseOptionToken(inner.slice(2), false);
|
||||||
const parts = inner.split(/\s+/);
|
if (result.isFlag) {
|
||||||
const name = parts[0].slice(2);
|
schema.flags.push({ name: result.name, short: result.short });
|
||||||
|
|
||||||
// 检查是否有类型定义(如 --flag: boolean 或 --opt: string[])
|
|
||||||
if (name.includes(':')) {
|
|
||||||
const [optName, typeStr] = name.split(':').map(s => s.trim());
|
|
||||||
const parsedSchema = defineSchema(typeStr);
|
|
||||||
schema.options.push({
|
|
||||||
name: optName,
|
|
||||||
required: false,
|
|
||||||
schema: parsedSchema,
|
|
||||||
});
|
|
||||||
} else if (parts.length > 1) {
|
|
||||||
// 可选选项(旧语法:--opt <value>)
|
|
||||||
const valueToken = parts[1];
|
|
||||||
let typeStr = valueToken;
|
|
||||||
// 如果是 <value> 格式,提取类型
|
|
||||||
if (valueToken.startsWith('<') && valueToken.endsWith('>')) {
|
|
||||||
typeStr = valueToken.slice(1, -1);
|
|
||||||
}
|
|
||||||
// 尝试解析为 inline-schema 类型
|
|
||||||
let parsedSchema: ParsedSchema | undefined;
|
|
||||||
try {
|
|
||||||
parsedSchema = defineSchema(typeStr);
|
|
||||||
} catch {
|
|
||||||
// 不是有效的 schema,使用默认字符串
|
|
||||||
}
|
|
||||||
schema.options.push({
|
|
||||||
name,
|
|
||||||
required: false,
|
|
||||||
schema: parsedSchema,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// 可选标志
|
schema.options.push({
|
||||||
schema.flags.push({ name });
|
name: result.name,
|
||||||
|
short: result.short,
|
||||||
|
required: false,
|
||||||
|
defaultValue: result.defaultValue,
|
||||||
|
schema: result.schema,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (inner.startsWith('-') && inner.length > 1) {
|
} else if (inner.startsWith('-') && inner.length > 1 && !inner.includes('--')) {
|
||||||
// 可选短格式标志或选项
|
const result = parseOptionToken(inner.slice(1), false);
|
||||||
const parts = inner.split(/\s+/);
|
if (result.isFlag) {
|
||||||
const short = parts[0].slice(1);
|
schema.flags.push({ name: result.name, short: result.short || result.name });
|
||||||
|
|
||||||
// 检查是否有类型定义
|
|
||||||
if (short.includes(':')) {
|
|
||||||
const [optName, typeStr] = short.split(':').map(s => s.trim());
|
|
||||||
const parsedSchema = defineSchema(typeStr);
|
|
||||||
schema.options.push({
|
|
||||||
name: optName,
|
|
||||||
short: optName,
|
|
||||||
required: false,
|
|
||||||
schema: parsedSchema,
|
|
||||||
});
|
|
||||||
} else if (parts.length > 1) {
|
|
||||||
// 可选选项(旧语法)
|
|
||||||
const valueToken = parts[1];
|
|
||||||
let typeStr = valueToken;
|
|
||||||
if (valueToken.startsWith('<') && valueToken.endsWith('>')) {
|
|
||||||
typeStr = valueToken.slice(1, -1);
|
|
||||||
}
|
|
||||||
let parsedSchema: ParsedSchema | undefined;
|
|
||||||
try {
|
|
||||||
parsedSchema = defineSchema(typeStr);
|
|
||||||
} catch {
|
|
||||||
// 不是有效的 schema,使用默认字符串
|
|
||||||
}
|
|
||||||
schema.options.push({
|
|
||||||
name: short,
|
|
||||||
short,
|
|
||||||
required: false,
|
|
||||||
schema: parsedSchema,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// 可选标志
|
schema.options.push({
|
||||||
schema.flags.push({ name: short, short });
|
name: result.name,
|
||||||
|
short: result.short || result.name,
|
||||||
|
required: false,
|
||||||
|
defaultValue: result.defaultValue,
|
||||||
|
schema: result.schema,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 可选参数
|
|
||||||
const isVariadic = inner.endsWith('...');
|
const isVariadic = inner.endsWith('...');
|
||||||
let paramContent = isVariadic ? inner.slice(0, -3) : inner;
|
let paramContent = isVariadic ? inner.slice(0, -3) : inner;
|
||||||
let parsedSchema: ParsedSchema | undefined;
|
let parsedSchema: ParsedSchema | undefined;
|
||||||
|
|
||||||
// 检查是否有类型定义(如 [name: string])
|
|
||||||
if (paramContent.includes(':')) {
|
if (paramContent.includes(':')) {
|
||||||
const [name, typeStr] = paramContent.split(':').map(s => s.trim());
|
const [name, typeStr] = paramContent.split(':').map(s => s.trim());
|
||||||
try {
|
try {
|
||||||
|
|
@ -332,86 +302,38 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
} else if (token.startsWith('--')) {
|
} else if (token.startsWith('--')) {
|
||||||
// 长格式标志或选项(必需的,因为不在方括号内)
|
const result = parseOptionToken(token.slice(2), true);
|
||||||
const name = token.slice(2);
|
if (result.isFlag) {
|
||||||
const nextToken = tokens[i + 1];
|
schema.flags.push({ name: result.name, short: result.short });
|
||||||
|
|
||||||
// 检查是否有类型定义(如 --flag: boolean)
|
|
||||||
if (name.includes(':')) {
|
|
||||||
const [optName, typeStr] = name.split(':').map(s => s.trim());
|
|
||||||
const parsedSchema = defineSchema(typeStr);
|
|
||||||
schema.options.push({
|
|
||||||
name: optName,
|
|
||||||
required: true,
|
|
||||||
schema: parsedSchema,
|
|
||||||
});
|
|
||||||
i++;
|
|
||||||
} else if (nextToken && nextToken.startsWith('<') && nextToken.endsWith('>')) {
|
|
||||||
// 旧语法:--opt <value>
|
|
||||||
const valueToken = nextToken;
|
|
||||||
const typeStr = valueToken.slice(1, -1);
|
|
||||||
let parsedSchema: ParsedSchema | undefined;
|
|
||||||
try {
|
|
||||||
parsedSchema = defineSchema(typeStr);
|
|
||||||
} catch {
|
|
||||||
// 不是有效的 schema
|
|
||||||
}
|
|
||||||
schema.options.push({
|
|
||||||
name,
|
|
||||||
required: true,
|
|
||||||
schema: parsedSchema,
|
|
||||||
});
|
|
||||||
i += 2;
|
|
||||||
} else {
|
} else {
|
||||||
// 否则是标志
|
schema.options.push({
|
||||||
schema.flags.push({ name });
|
name: result.name,
|
||||||
i++;
|
short: result.short,
|
||||||
|
required: true,
|
||||||
|
defaultValue: result.defaultValue,
|
||||||
|
schema: result.schema,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
i++;
|
||||||
} else if (token.startsWith('-') && token.length > 1 && !/^-?\d+$/.test(token)) {
|
} else if (token.startsWith('-') && token.length > 1 && !/^-?\d+$/.test(token)) {
|
||||||
// 短格式标志或选项(必需的,因为不在方括号内)
|
const result = parseOptionToken(token.slice(1), true);
|
||||||
const short = token.slice(1);
|
if (result.isFlag) {
|
||||||
const nextToken = tokens[i + 1];
|
schema.flags.push({ name: result.name, short: result.short || result.name });
|
||||||
|
|
||||||
// 检查是否有类型定义
|
|
||||||
if (short.includes(':')) {
|
|
||||||
const [optName, typeStr] = short.split(':').map(s => s.trim());
|
|
||||||
const parsedSchema = defineSchema(typeStr);
|
|
||||||
schema.options.push({
|
|
||||||
name: optName,
|
|
||||||
short: optName,
|
|
||||||
required: true,
|
|
||||||
schema: parsedSchema,
|
|
||||||
});
|
|
||||||
i++;
|
|
||||||
} else if (nextToken && nextToken.startsWith('<') && nextToken.endsWith('>')) {
|
|
||||||
// 旧语法
|
|
||||||
const valueToken = nextToken;
|
|
||||||
const typeStr = valueToken.slice(1, -1);
|
|
||||||
let parsedSchema: ParsedSchema | undefined;
|
|
||||||
try {
|
|
||||||
parsedSchema = defineSchema(typeStr);
|
|
||||||
} catch {
|
|
||||||
// 不是有效的 schema
|
|
||||||
}
|
|
||||||
schema.options.push({
|
|
||||||
name: short,
|
|
||||||
short,
|
|
||||||
required: true,
|
|
||||||
schema: parsedSchema,
|
|
||||||
});
|
|
||||||
i += 2;
|
|
||||||
} else {
|
} else {
|
||||||
// 否则是标志
|
schema.options.push({
|
||||||
schema.flags.push({ name: short, short });
|
name: result.name,
|
||||||
i++;
|
short: result.short || result.name,
|
||||||
|
required: true,
|
||||||
|
defaultValue: result.defaultValue,
|
||||||
|
schema: result.schema,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
i++;
|
||||||
} else if (token.startsWith('<') && token.endsWith('>')) {
|
} else if (token.startsWith('<') && token.endsWith('>')) {
|
||||||
// 必需参数
|
|
||||||
const isVariadic = token.endsWith('...>');
|
const isVariadic = token.endsWith('...>');
|
||||||
let paramContent = token.replace(/^[<]+|[>.>]+$/g, '');
|
let paramContent = token.replace(/^[<]+|[>.>]+$/g, '');
|
||||||
let parsedSchema: ParsedSchema | undefined;
|
let parsedSchema: ParsedSchema | undefined;
|
||||||
|
|
||||||
// 检查是否有类型定义(如 <from: [x: string; y: string]>)
|
|
||||||
if (paramContent.includes(':')) {
|
if (paramContent.includes(':')) {
|
||||||
const colonIndex = paramContent.indexOf(':');
|
const colonIndex = paramContent.indexOf(':');
|
||||||
const name = paramContent.slice(0, colonIndex).trim();
|
const name = paramContent.slice(0, colonIndex).trim();
|
||||||
|
|
@ -432,7 +354,6 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
||||||
});
|
});
|
||||||
i++;
|
i++;
|
||||||
} else {
|
} else {
|
||||||
// 跳过无法识别的 token
|
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -440,6 +361,92 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
||||||
return schema;
|
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> 或 [value])
|
* 检查 token 是否是值占位符(如 <value> 或 [value])
|
||||||
*/
|
*/
|
||||||
|
|
@ -464,6 +471,7 @@ function isParamPlaceholder(token: string): boolean {
|
||||||
/**
|
/**
|
||||||
* 将 schema 字符串分解为 tokens
|
* 将 schema 字符串分解为 tokens
|
||||||
* 支持方括号分组:[...args] [--flag] 等
|
* 支持方括号分组:[...args] [--flag] 等
|
||||||
|
* 支持尖括号分组:<param> <param: type> 等
|
||||||
*/
|
*/
|
||||||
function tokenizeSchema(input: string): string[] {
|
function tokenizeSchema(input: string): string[] {
|
||||||
const tokens: string[] = [];
|
const tokens: string[] = [];
|
||||||
|
|
@ -477,13 +485,11 @@ function tokenizeSchema(input: string): string[] {
|
||||||
|
|
||||||
if (inBracket) {
|
if (inBracket) {
|
||||||
if (char === ']') {
|
if (char === ']') {
|
||||||
// 结束括号,将内容加上括号作为一个 token
|
|
||||||
tokens.push(`[${bracketContent}]`);
|
tokens.push(`[${bracketContent}]`);
|
||||||
inBracket = false;
|
inBracket = false;
|
||||||
bracketContent = '';
|
bracketContent = '';
|
||||||
current = '';
|
current = '';
|
||||||
} else if (char === '[') {
|
} else if (char === '[') {
|
||||||
// 嵌套括号(不支持)
|
|
||||||
bracketContent += char;
|
bracketContent += char;
|
||||||
} else {
|
} else {
|
||||||
bracketContent += char;
|
bracketContent += char;
|
||||||
|
|
@ -501,7 +507,6 @@ function tokenizeSchema(input: string): string[] {
|
||||||
inBracket = true;
|
inBracket = true;
|
||||||
bracketContent = '';
|
bracketContent = '';
|
||||||
} else if (char === '<') {
|
} else if (char === '<') {
|
||||||
// 尖括号内容作为一个整体
|
|
||||||
let angleContent = '<';
|
let angleContent = '<';
|
||||||
i++;
|
i++;
|
||||||
while (i < input.length && input[i] !== '>') {
|
while (i < input.length && input[i] !== '>') {
|
||||||
|
|
@ -521,7 +526,6 @@ function tokenizeSchema(input: string): string[] {
|
||||||
tokens.push(current);
|
tokens.push(current);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理未闭合的括号
|
|
||||||
if (bracketContent.length > 0) {
|
if (bracketContent.length > 0) {
|
||||||
tokens.push(`[${bracketContent}`);
|
tokens.push(`[${bracketContent}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,11 @@ describe('parseCommandSchema with inline-schema', () => {
|
||||||
it('should parse schema with typed options', () => {
|
it('should parse schema with typed options', () => {
|
||||||
const schema = parseCommandSchema('move <from> <to> [--all: boolean] [--count: number]');
|
const schema = parseCommandSchema('move <from> <to> [--all: boolean] [--count: number]');
|
||||||
expect(schema.name).toBe('move');
|
expect(schema.name).toBe('move');
|
||||||
expect(schema.options).toHaveLength(2);
|
expect(schema.flags).toHaveLength(1);
|
||||||
expect(schema.options[0].name).toBe('all');
|
expect(schema.options).toHaveLength(1);
|
||||||
|
expect(schema.flags[0].name).toBe('all');
|
||||||
|
expect(schema.options[0].name).toBe('count');
|
||||||
expect(schema.options[0].schema).toBeDefined();
|
expect(schema.options[0].schema).toBeDefined();
|
||||||
expect(schema.options[1].name).toBe('count');
|
|
||||||
expect(schema.options[1].schema).toBeDefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse schema with tuple type', () => {
|
it('should parse schema with tuple type', () => {
|
||||||
|
|
@ -54,11 +54,11 @@ describe('parseCommandSchema with inline-schema', () => {
|
||||||
|
|
||||||
it('should parse schema with mixed types', () => {
|
it('should parse schema with mixed types', () => {
|
||||||
const schema = parseCommandSchema(
|
const schema = parseCommandSchema(
|
||||||
'move <from: [x: string; y: string]> <to: string> [--all: boolean] [--count: number]'
|
'move <from: [x: string; y: string]> <to: string> [--count: number]'
|
||||||
);
|
);
|
||||||
expect(schema.name).toBe('move');
|
expect(schema.name).toBe('move');
|
||||||
expect(schema.params).toHaveLength(2);
|
expect(schema.params).toHaveLength(2);
|
||||||
expect(schema.options).toHaveLength(2);
|
expect(schema.options).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse schema with optional typed param', () => {
|
it('should parse schema with optional typed param', () => {
|
||||||
|
|
@ -94,17 +94,6 @@ describe('parseCommandWithSchema', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse and validate command with boolean option', () => {
|
it('should parse and validate command with boolean option', () => {
|
||||||
const result = parseCommandWithSchema(
|
|
||||||
'move meeple1 region1 --all true',
|
|
||||||
'move <from> <to> [--all: boolean]'
|
|
||||||
);
|
|
||||||
expect(result.valid).toBe(true);
|
|
||||||
if (result.valid) {
|
|
||||||
expect(result.command.options.all).toBe(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse and validate command with number option', () => {
|
|
||||||
const result = parseCommandWithSchema(
|
const result = parseCommandWithSchema(
|
||||||
'move meeple1 region1 --count 5',
|
'move meeple1 region1 --count 5',
|
||||||
'move <from> <to> [--count: number]'
|
'move <from> <to> [--count: number]'
|
||||||
|
|
@ -115,6 +104,17 @@ describe('parseCommandWithSchema', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should parse and validate command with number option', () => {
|
||||||
|
const result = parseCommandWithSchema(
|
||||||
|
'move meeple1 region1 --speed 100',
|
||||||
|
'move <from> <to> [--speed: number]'
|
||||||
|
);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
if (result.valid) {
|
||||||
|
expect(result.command.options.speed).toBe(100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('should fail validation with wrong command name', () => {
|
it('should fail validation with wrong command name', () => {
|
||||||
const result = parseCommandWithSchema(
|
const result = parseCommandWithSchema(
|
||||||
'jump meeple1 region1',
|
'jump meeple1 region1',
|
||||||
|
|
@ -144,22 +144,21 @@ describe('parseCommandWithSchema', () => {
|
||||||
it('should fail validation with missing required option', () => {
|
it('should fail validation with missing required option', () => {
|
||||||
const result = parseCommandWithSchema(
|
const result = parseCommandWithSchema(
|
||||||
'move meeple1 region1',
|
'move meeple1 region1',
|
||||||
'move <from> <to> [--force: boolean]'
|
'move <from> <to> [--force]'
|
||||||
);
|
);
|
||||||
// 可选选项,应该通过验证
|
// 可选标志,应该通过验证
|
||||||
expect(result.valid).toBe(true);
|
expect(result.valid).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse complex command with typed params and options', () => {
|
it('should parse complex command with typed params and options', () => {
|
||||||
const result = parseCommandWithSchema(
|
const result = parseCommandWithSchema(
|
||||||
'move [1; 2] region1 --all true --count 3',
|
'move [1; 2] region1 --count 3',
|
||||||
'move <from: [x: string; y: string]> <to: string> [--all: boolean] [--count: number]'
|
'move <from: [x: string; y: string]> <to: string> [--count: number]'
|
||||||
);
|
);
|
||||||
expect(result.valid).toBe(true);
|
expect(result.valid).toBe(true);
|
||||||
if (result.valid) {
|
if (result.valid) {
|
||||||
expect(result.command.params[0]).toEqual(['1', '2']);
|
expect(result.command.params[0]).toEqual(['1', '2']);
|
||||||
expect(result.command.params[1]).toBe('region1');
|
expect(result.command.params[1]).toBe('region1');
|
||||||
expect(result.command.options.all).toBe(true);
|
|
||||||
expect(result.command.options.count).toBe(3);
|
expect(result.command.options.count).toBe(3);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -207,8 +206,8 @@ describe('validateCommand with schema types', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate command with typed options', () => {
|
it('should validate command with typed options', () => {
|
||||||
const schema = parseCommandSchema('move <from> <to> [--all: boolean]');
|
const schema = parseCommandSchema('move <from> <to> [--count: number]');
|
||||||
const command = parseCommand('move meeple1 region1 --all true');
|
const command = parseCommand('move meeple1 region1 --count 5');
|
||||||
const result = validateCommand(command, schema);
|
const result = validateCommand(command, schema);
|
||||||
expect(result.valid).toBe(true);
|
expect(result.valid).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -70,42 +70,41 @@ describe('parseCommandSchema', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse long options', () => {
|
it('should parse long options', () => {
|
||||||
const schema = parseCommandSchema('move --x <value> [--y value]');
|
const schema = parseCommandSchema('move --x: string [--y: string]');
|
||||||
expect(schema.options).toEqual([
|
expect(schema.options).toEqual([
|
||||||
{ name: 'x', required: true },
|
{ name: 'x', required: true, schema: expect.any(Object) },
|
||||||
{ name: 'y', required: false },
|
{ name: 'y', required: false, schema: expect.any(Object) },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse short options', () => {
|
it('should parse short options', () => {
|
||||||
const schema = parseCommandSchema('move -x <value> [-y value]');
|
const schema = parseCommandSchema('move -x: string [-y: string]');
|
||||||
expect(schema.options).toEqual([
|
expect(schema.options).toEqual([
|
||||||
{ name: 'x', short: 'x', required: true },
|
{ name: 'x', short: 'x', required: true, schema: expect.any(Object) },
|
||||||
{ name: 'y', short: 'y', required: false },
|
{ name: 'y', short: 'y', required: false, schema: expect.any(Object) },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse mixed schema', () => {
|
it('should parse mixed schema', () => {
|
||||||
const schema = parseCommandSchema('move <from> <to> [--force] [-f] [--speed <val>] [-s val]');
|
const schema = parseCommandSchema('move <from> <to> [--force] [-f] [--speed: string -s]');
|
||||||
expect(schema).toEqual({
|
expect(schema).toEqual({
|
||||||
name: 'move',
|
name: 'move',
|
||||||
params: [
|
params: [
|
||||||
{ name: 'from', required: true, variadic: false },
|
{ name: 'from', required: true, variadic: false, schema: undefined },
|
||||||
{ name: 'to', required: true, variadic: false },
|
{ name: 'to', required: true, variadic: false, schema: undefined },
|
||||||
],
|
],
|
||||||
flags: [
|
flags: [
|
||||||
{ name: 'force' },
|
{ name: 'force' },
|
||||||
{ name: 'f', short: 'f' },
|
{ name: 'f', short: 'f' },
|
||||||
],
|
],
|
||||||
options: [
|
options: [
|
||||||
{ name: 'speed', required: false },
|
{ name: 'speed', short: 's', required: false, schema: expect.any(Object), defaultValue: undefined },
|
||||||
{ name: 's', short: 's', required: false },
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle complex schema', () => {
|
it('should handle complex schema', () => {
|
||||||
const schema = parseCommandSchema('place <piece> <region> [x...] [--rotate <angle>] [--force] [-f]');
|
const schema = parseCommandSchema('place <piece> <region> [x...] [--rotate: number] [--force] [-f]');
|
||||||
expect(schema.name).toBe('place');
|
expect(schema.name).toBe('place');
|
||||||
expect(schema.params).toHaveLength(3);
|
expect(schema.params).toHaveLength(3);
|
||||||
expect(schema.flags).toHaveLength(2);
|
expect(schema.flags).toHaveLength(2);
|
||||||
|
|
@ -172,7 +171,7 @@ describe('validateCommand', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject missing required option', () => {
|
it('should reject missing required option', () => {
|
||||||
const schema = parseCommandSchema('move <from> --speed <val>');
|
const schema = parseCommandSchema('move <from> --speed: string');
|
||||||
const command = parseCommand('move meeple1');
|
const command = parseCommand('move meeple1');
|
||||||
const result = validateCommand(command, schema);
|
const result = validateCommand(command, schema);
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
|
|
@ -184,14 +183,14 @@ describe('validateCommand', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept present required option', () => {
|
it('should accept present required option', () => {
|
||||||
const schema = parseCommandSchema('move <from> --speed <val>');
|
const schema = parseCommandSchema('move <from> --speed: string');
|
||||||
const command = parseCommand('move meeple1 --speed 10');
|
const command = parseCommand('move meeple1 --speed 10');
|
||||||
const result = validateCommand(command, schema);
|
const result = validateCommand(command, schema);
|
||||||
expect(result).toEqual({ valid: true });
|
expect(result).toEqual({ valid: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept optional option missing', () => {
|
it('should accept optional option missing', () => {
|
||||||
const schema = parseCommandSchema('move <from> [--speed [val]]');
|
const schema = parseCommandSchema('move <from> [--speed: string]');
|
||||||
const command = parseCommand('move meeple1');
|
const command = parseCommand('move meeple1');
|
||||||
const result = validateCommand(command, schema);
|
const result = validateCommand(command, schema);
|
||||||
expect(result).toEqual({ valid: true });
|
expect(result).toEqual({ valid: true });
|
||||||
|
|
@ -206,14 +205,14 @@ describe('validateCommand', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate short form option', () => {
|
it('should validate short form option', () => {
|
||||||
const schema = parseCommandSchema('move <from> -s <val>');
|
const schema = parseCommandSchema('move <from> -s: string');
|
||||||
const command = parseCommand('move meeple1 -s 10');
|
const command = parseCommand('move meeple1 -s 10');
|
||||||
const result = validateCommand(command, schema);
|
const result = validateCommand(command, schema);
|
||||||
expect(result).toEqual({ valid: true });
|
expect(result).toEqual({ valid: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should provide detailed error messages', () => {
|
it('should provide detailed error messages', () => {
|
||||||
const schema = parseCommandSchema('place <piece> <region> --rotate <angle>');
|
const schema = parseCommandSchema('place <piece> <region> --rotate: string');
|
||||||
const command = parseCommand('place meeple1');
|
const command = parseCommand('place meeple1');
|
||||||
const result = validateCommand(command, schema);
|
const result = validateCommand(command, schema);
|
||||||
expect(result.valid).toBe(false);
|
expect(result.valid).toBe(false);
|
||||||
|
|
@ -225,7 +224,7 @@ describe('validateCommand', () => {
|
||||||
|
|
||||||
describe('integration', () => {
|
describe('integration', () => {
|
||||||
it('should work together parse and validate', () => {
|
it('should work together parse and validate', () => {
|
||||||
const schemaStr = 'place <piece> <region> [--x <val>] [--y [val]] [--force] [-f]';
|
const schemaStr = 'place <piece> <region> [--x: string] [--y: string] [--force] [-f]';
|
||||||
const schema = parseCommandSchema(schemaStr);
|
const schema = parseCommandSchema(schemaStr);
|
||||||
|
|
||||||
const validCmd = parseCommand('place meeple1 board --x 5 --force');
|
const validCmd = parseCommand('place meeple1 board --x 5 --force');
|
||||||
|
|
@ -235,4 +234,57 @@ describe('integration', () => {
|
||||||
const result = validateCommand(invalidCmd, schema);
|
const result = validateCommand(invalidCmd, schema);
|
||||||
expect(result.valid).toBe(false);
|
expect(result.valid).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should parse short alias syntax', () => {
|
||||||
|
const schema = parseCommandSchema('move <from> [--verbose: boolean -v]');
|
||||||
|
expect(schema.flags).toHaveLength(1);
|
||||||
|
expect(schema.flags[0]).toEqual({ name: 'verbose', short: 'v' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse short alias for options', () => {
|
||||||
|
const schema = parseCommandSchema('move <from> [--speed: number -s]');
|
||||||
|
expect(schema.options).toHaveLength(1);
|
||||||
|
expect(schema.options[0]).toEqual({
|
||||||
|
name: 'speed',
|
||||||
|
short: 's',
|
||||||
|
required: false,
|
||||||
|
schema: expect.any(Object),
|
||||||
|
defaultValue: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse default value syntax', () => {
|
||||||
|
const schema = parseCommandSchema('move <from> [--speed: number = 10]');
|
||||||
|
expect(schema.options).toHaveLength(1);
|
||||||
|
expect(schema.options[0].defaultValue).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse default string value', () => {
|
||||||
|
const schema = parseCommandSchema('move <from> [--name: string = "default"]');
|
||||||
|
expect(schema.options).toHaveLength(1);
|
||||||
|
expect(schema.options[0].defaultValue).toBe('default');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse short alias with default value', () => {
|
||||||
|
const schema = parseCommandSchema('move <from> [--speed: number -s = 5]');
|
||||||
|
expect(schema.options).toHaveLength(1);
|
||||||
|
expect(schema.options[0].short).toBe('s');
|
||||||
|
expect(schema.options[0].defaultValue).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse command with short alias', () => {
|
||||||
|
const schema = parseCommandSchema('move <from> [--verbose -v]');
|
||||||
|
const command = parseCommand('move meeple1 -v');
|
||||||
|
const result = validateCommand(command, schema);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
expect(command.flags.v).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse command with short alias option', () => {
|
||||||
|
const schema = parseCommandSchema('move <from> [--speed: number -s]');
|
||||||
|
const command = parseCommand('move meeple1 -s 100');
|
||||||
|
const result = validateCommand(command, schema);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
expect(command.options.s).toBe('100');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue