refactor: command syntax improvement
This commit is contained in:
+138
-139
@@ -210,11 +210,13 @@ function tokenize(input: string): string[] {
|
||||
* - [param...] 可选可变参数
|
||||
* - <param: type> 带类型定义的必需参数
|
||||
* - [param: type] 带类型定义的可选参数
|
||||
* - --flag 长格式标志
|
||||
* - --flag 长格式标志(布尔类型)
|
||||
* - --flag: boolean 长格式标志(布尔类型,与上面等价)
|
||||
* - -f 短格式标志
|
||||
* - --option <value> 长格式选项
|
||||
* - --option: type 带类型的长格式选项
|
||||
* - -o <value> 短格式选项
|
||||
* - --option: type = default 带默认值的选项
|
||||
* - --option: type -o 带短别名的选项
|
||||
* - --option: type -o = default 带短别名和默认值的选项
|
||||
* - -o: type 带类型的短格式选项
|
||||
*
|
||||
* 类型语法使用 inline-schema 格式(使用 ; 而非 ,):
|
||||
@@ -224,8 +226,9 @@ function tokenize(input: string): string[] {
|
||||
* - [string; number][] 元组数组
|
||||
*
|
||||
* @example
|
||||
* parseCommandSchema('move <from> [to...] [--force] [-f] [--speed <val>]')
|
||||
* parseCommandSchema('move <from: [x: string; y: string]> <to: string> [--all: boolean]')
|
||||
* parseCommandSchema('move <from> [to...] [--force] [-f] [--speed: number]')
|
||||
* parseCommandSchema('move <from: [x: string; y: string]> <to: string> [--all]')
|
||||
* parseCommandSchema('move <from> <to> [--speed: number = 10 -s]')
|
||||
*/
|
||||
export function parseCommandSchema(schemaStr: string): CommandSchema {
|
||||
const schema: CommandSchema = {
|
||||
@@ -240,7 +243,6 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
||||
return schema;
|
||||
}
|
||||
|
||||
// 第一个 token 是命令名称
|
||||
schema.name = tokens[0];
|
||||
|
||||
let i = 1;
|
||||
@@ -248,77 +250,39 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
||||
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);
|
||||
|
||||
if (name.endsWith(':')) {
|
||||
const optName = name.slice(0, -1).trim();
|
||||
const typeStr = parts[1] || '';
|
||||
const parsedSchema = defineSchema(typeStr);
|
||||
schema.options.push({
|
||||
name: optName,
|
||||
required: false,
|
||||
schema: parsedSchema,
|
||||
});
|
||||
} else 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) {
|
||||
schema.options.push({
|
||||
name,
|
||||
required: false,
|
||||
});
|
||||
const result = parseOptionToken(inner.slice(2), false);
|
||||
if (result.isFlag) {
|
||||
schema.flags.push({ name: result.name, short: result.short });
|
||||
} else {
|
||||
schema.flags.push({ name });
|
||||
schema.options.push({
|
||||
name: result.name,
|
||||
short: result.short,
|
||||
required: false,
|
||||
defaultValue: result.defaultValue,
|
||||
schema: result.schema,
|
||||
});
|
||||
}
|
||||
} else if (inner.startsWith('-') && inner.length > 1) {
|
||||
const parts = inner.split(/\s+/);
|
||||
const short = parts[0].slice(1);
|
||||
|
||||
if (short.endsWith(':')) {
|
||||
const optName = short.slice(0, -1).trim();
|
||||
const typeStr = parts[1] || '';
|
||||
const parsedSchema = defineSchema(typeStr);
|
||||
schema.options.push({
|
||||
name: optName,
|
||||
short: optName,
|
||||
required: false,
|
||||
schema: parsedSchema,
|
||||
});
|
||||
} else 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) {
|
||||
schema.options.push({
|
||||
name: short,
|
||||
short,
|
||||
required: false,
|
||||
});
|
||||
} 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.flags.push({ name: short, short });
|
||||
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;
|
||||
|
||||
// 检查是否有类型定义(如 [name: string])
|
||||
if (paramContent.includes(':')) {
|
||||
const [name, typeStr] = paramContent.split(':').map(s => s.trim());
|
||||
try {
|
||||
@@ -338,85 +302,38 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
||||
}
|
||||
i++;
|
||||
} else if (token.startsWith('--')) {
|
||||
// 长格式标志或选项(必需的,因为不在方括号内)
|
||||
const name = token.slice(2);
|
||||
const nextToken = tokens[i + 1];
|
||||
|
||||
if (name.endsWith(':')) {
|
||||
const optName = name.slice(0, -1).trim();
|
||||
const nextPart = tokens[i + 1];
|
||||
const typeStr = nextPart || '';
|
||||
const parsedSchema = defineSchema(typeStr);
|
||||
schema.options.push({
|
||||
name: optName,
|
||||
required: true,
|
||||
schema: parsedSchema,
|
||||
});
|
||||
i += 2;
|
||||
} else 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('>')) {
|
||||
schema.options.push({
|
||||
name,
|
||||
required: true,
|
||||
});
|
||||
i += 2;
|
||||
const result = parseOptionToken(token.slice(2), true);
|
||||
if (result.isFlag) {
|
||||
schema.flags.push({ name: result.name, short: result.short });
|
||||
} else {
|
||||
schema.flags.push({ name });
|
||||
i++;
|
||||
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 short = token.slice(1);
|
||||
const nextToken = tokens[i + 1];
|
||||
|
||||
if (short.endsWith(':')) {
|
||||
const optName = short.slice(0, -1).trim();
|
||||
const nextPart = tokens[i + 1];
|
||||
const typeStr = nextPart || '';
|
||||
const parsedSchema = defineSchema(typeStr);
|
||||
schema.options.push({
|
||||
name: optName,
|
||||
short: optName,
|
||||
required: true,
|
||||
schema: parsedSchema,
|
||||
});
|
||||
i += 2;
|
||||
} else 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('>')) {
|
||||
schema.options.push({
|
||||
name: short,
|
||||
short,
|
||||
required: true,
|
||||
});
|
||||
i += 2;
|
||||
const result = parseOptionToken(token.slice(1), true);
|
||||
if (result.isFlag) {
|
||||
schema.flags.push({ name: result.name, short: result.short || result.name });
|
||||
} else {
|
||||
schema.flags.push({ name: short, short });
|
||||
i++;
|
||||
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, '');
|
||||
let parsedSchema: ParsedSchema | undefined;
|
||||
|
||||
// 检查是否有类型定义(如 <from: [x: string; y: string]>)
|
||||
if (paramContent.includes(':')) {
|
||||
const colonIndex = paramContent.indexOf(':');
|
||||
const name = paramContent.slice(0, colonIndex).trim();
|
||||
@@ -437,7 +354,6 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
||||
});
|
||||
i++;
|
||||
} else {
|
||||
// 跳过无法识别的 token
|
||||
i++;
|
||||
}
|
||||
}
|
||||
@@ -445,6 +361,92 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
|
||||
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])
|
||||
*/
|
||||
@@ -469,6 +471,7 @@ function isParamPlaceholder(token: string): boolean {
|
||||
/**
|
||||
* 将 schema 字符串分解为 tokens
|
||||
* 支持方括号分组:[...args] [--flag] 等
|
||||
* 支持尖括号分组:<param> <param: type> 等
|
||||
*/
|
||||
function tokenizeSchema(input: string): string[] {
|
||||
const tokens: string[] = [];
|
||||
@@ -482,13 +485,11 @@ function tokenizeSchema(input: string): string[] {
|
||||
|
||||
if (inBracket) {
|
||||
if (char === ']') {
|
||||
// 结束括号,将内容加上括号作为一个 token
|
||||
tokens.push(`[${bracketContent}]`);
|
||||
inBracket = false;
|
||||
bracketContent = '';
|
||||
current = '';
|
||||
} else if (char === '[') {
|
||||
// 嵌套括号(不支持)
|
||||
bracketContent += char;
|
||||
} else {
|
||||
bracketContent += char;
|
||||
@@ -506,7 +507,6 @@ function tokenizeSchema(input: string): string[] {
|
||||
inBracket = true;
|
||||
bracketContent = '';
|
||||
} else if (char === '<') {
|
||||
// 尖括号内容作为一个整体
|
||||
let angleContent = '<';
|
||||
i++;
|
||||
while (i < input.length && input[i] !== '>') {
|
||||
@@ -526,7 +526,6 @@ function tokenizeSchema(input: string): string[] {
|
||||
tokens.push(current);
|
||||
}
|
||||
|
||||
// 处理未闭合的括号
|
||||
if (bracketContent.length > 0) {
|
||||
tokens.push(`[${bracketContent}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user