refactor: command syntax improvement

This commit is contained in:
2026-04-01 21:18:58 +08:00
parent d1ea04c442
commit 033fb6c894
3 changed files with 231 additions and 181 deletions
+138 -139
View File
@@ -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}`);
}