164 lines
3.6 KiB
TypeScript
164 lines
3.6 KiB
TypeScript
import type { ParsedCliCommand, CliCommandArgs } from './CliCommand';
|
|
|
|
/**
|
|
* 命令解析错误
|
|
*/
|
|
export class CommandParseError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = 'CommandParseError';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* CLI 命令解析器
|
|
* 解析 CLI 风格的命令字符串
|
|
*/
|
|
export class CommandParser {
|
|
/**
|
|
* 解析命令字符串
|
|
* @param input 命令字符串,如 "move card-1 discard --faceup=true"
|
|
*/
|
|
parse(input: string): ParsedCliCommand {
|
|
const trimmed = input.trim();
|
|
|
|
if (!trimmed) {
|
|
throw new CommandParseError('Empty command');
|
|
}
|
|
|
|
const tokens = this.tokenize(trimmed);
|
|
|
|
if (tokens.length === 0) {
|
|
throw new CommandParseError('No command found');
|
|
}
|
|
|
|
const commandName = tokens[0];
|
|
const args = this.parseArgs(tokens.slice(1));
|
|
|
|
return {
|
|
commandName,
|
|
args,
|
|
raw: input,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 将命令字符串分词
|
|
*/
|
|
private tokenize(input: string): string[] {
|
|
const tokens: string[] = [];
|
|
let current = '';
|
|
let inQuotes = false;
|
|
let quoteChar = '';
|
|
|
|
for (let i = 0; i < input.length; i++) {
|
|
const char = input[i];
|
|
|
|
if (inQuotes) {
|
|
if (char === quoteChar) {
|
|
tokens.push(current);
|
|
current = '';
|
|
inQuotes = false;
|
|
} else {
|
|
current += char;
|
|
}
|
|
} else if (char === '"' || char === "'") {
|
|
inQuotes = true;
|
|
quoteChar = char;
|
|
} else if (char === ' ' || char === '\t') {
|
|
if (current) {
|
|
tokens.push(current);
|
|
current = '';
|
|
}
|
|
} else {
|
|
current += char;
|
|
}
|
|
}
|
|
|
|
if (current) {
|
|
tokens.push(current);
|
|
}
|
|
|
|
if (inQuotes) {
|
|
throw new CommandParseError('Unclosed quote in command');
|
|
}
|
|
|
|
return tokens;
|
|
}
|
|
|
|
/**
|
|
* 解析参数
|
|
*/
|
|
private parseArgs(tokens: string[]): CliCommandArgs {
|
|
const positional: string[] = [];
|
|
const flags: Record<string, string | boolean> = {};
|
|
|
|
for (const token of tokens) {
|
|
if (token.startsWith('--')) {
|
|
// 长标志 --key=value 或 --flag
|
|
const flagMatch = token.match(/^--([^=]+)(?:=(.+))?$/);
|
|
if (flagMatch) {
|
|
const [, key, value] = flagMatch;
|
|
flags[key] = value !== undefined ? this.parseFlagValue(value) : true;
|
|
}
|
|
} else if (token.startsWith('-') && token.length === 2) {
|
|
// 短标志 -f 或 -k=v
|
|
const flagMatch = token.match(/^-([^=]+)(?:=(.+))?$/);
|
|
if (flagMatch) {
|
|
const [, key, value] = flagMatch;
|
|
flags[key] = value !== undefined ? this.parseFlagValue(value) : true;
|
|
}
|
|
} else {
|
|
// 位置参数
|
|
positional.push(token);
|
|
}
|
|
}
|
|
|
|
return { positional, flags };
|
|
}
|
|
|
|
/**
|
|
* 解析标志值
|
|
*/
|
|
private parseFlagValue(value: string): string | boolean {
|
|
// 布尔值
|
|
if (value.toLowerCase() === 'true') return true;
|
|
if (value.toLowerCase() === 'false') return false;
|
|
|
|
// 数字转换为字符串
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* 格式化命令用于显示
|
|
*/
|
|
static formatCommand(name: string, args?: CliCommandArgs): string {
|
|
if (!args) {
|
|
return name;
|
|
}
|
|
|
|
const parts = [name];
|
|
|
|
// 添加位置参数
|
|
parts.push(...args.positional);
|
|
|
|
// 添加标志参数
|
|
for (const [key, value] of Object.entries(args.flags)) {
|
|
if (value === true) {
|
|
parts.push(`--${key}`);
|
|
} else {
|
|
parts.push(`--${key}=${value}`);
|
|
}
|
|
}
|
|
|
|
return parts.join(' ');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 创建命令解析器
|
|
*/
|
|
export function createCommandParser(): CommandParser {
|
|
return new CommandParser();
|
|
}
|