refactor: break up command.ts

This commit is contained in:
hypercross 2026-04-01 22:37:15 +08:00
parent ad0d349090
commit 4761806a02
5 changed files with 539 additions and 662 deletions

View File

@ -1,662 +1,10 @@
import { defineSchema, type ParsedSchema, ParseError } from 'inline-schema';
export type Command = {
name: string;
flags: Record<string, true>;
options: Record<string, unknown>;
params: unknown[];
}
/**
* schema
*/
export type CommandParamSchema = {
/** 参数名称 */
name: string;
/** 是否必需 */
required: boolean;
/** 是否可变参数(可以接收多个值) */
variadic: boolean;
/** 参数类型 schema用于解析和验证 */
schema?: ParsedSchema;
}
/**
* schema
*/
export type CommandOptionSchema = {
/** 选项名称(长格式,不含 -- */
name: string;
/** 短格式名称(不含 - */
short?: string;
/** 是否必需 */
required: boolean;
/** 默认值 */
defaultValue?: unknown;
/** 选项类型 schema用于解析和验证 */
schema?: ParsedSchema;
}
/**
* 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...]
* (') (")
* 使 (\)
*
* @example
* parseCommand("move meeple1 region1 --force -x 10")
* // returns { name: "move", params: ["meeple1", "region1"], flags: { force: true }, options: { x: "10" } }
* parseCommand('place tile "large castle" --x 5')
* // returns { name: "place", params: ["tile", "large castle"], flags: {}, options: { x: "5" } }
*/
export function parseCommand(input: string): Command {
const tokens = tokenize(input);
if (tokens.length === 0) {
return { name: '', flags: {}, options: {}, params: [] };
}
const name = tokens[0];
const params: unknown[] = [];
const flags: Record<string, true> = {};
const options: Record<string, unknown> = {};
let i = 1;
while (i < tokens.length) {
const token = tokens[i];
if (token.startsWith('--') && !/^-?\d+$/.test(token)) {
// 长格式标志或选项:--flag 或 --option value
const key = token.slice(2);
const nextToken = tokens[i + 1];
// 如果下一个 token 存在且不以 - 开头(或者是负数),则是选项值
if (nextToken && (!nextToken.startsWith('-') || /^-\d+$/.test(nextToken))) {
options[key] = nextToken;
i += 2;
} else {
// 否则是布尔标志
flags[key] = true;
i++;
}
} else if (token.startsWith('-') && token.length > 1 && !/^-?\d+$/.test(token)) {
// 短格式标志或选项:-f 或 -o value但不匹配负数
const key = token.slice(1);
const nextToken = tokens[i + 1];
// 如果下一个 token 存在且不以 - 开头(或者是负数),则是选项值
if (nextToken && (!nextToken.startsWith('-') || /^-\d+$/.test(nextToken))) {
options[key] = nextToken;
i += 2;
} else {
// 否则是布尔标志
flags[key] = true;
i++;
}
} else {
// 普通参数(包括负数)
params.push(token);
i++;
}
}
return { name, flags, options, params };
}
/**
* tokens
*/
function tokenize(input: string): string[] {
const tokens: string[] = [];
let current = '';
let inQuote: string | null = null;
let inBracket = false;
let bracketDepth = 0;
let escaped = false;
let i = 0;
while (i < input.length) {
const char = input[i];
if (escaped) {
current += char;
escaped = false;
} else if (char === '\\') {
escaped = true;
} else if (inQuote) {
if (char === inQuote) {
inQuote = null;
} else {
current += char;
}
} else if (char === '"' || 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)) {
if (inBracket) {
current += char;
} else if (current.length > 0) {
tokens.push(current);
current = '';
}
} else {
current += char;
}
i++;
}
if (current.length > 0) {
tokens.push(current);
}
return tokens;
}
/**
* schema CommandSchema
*
* - <param>
* - [param]
* - <param...>
* - [param...]
* - <param: type>
* - [param: type]
* - --flag
* - --flag: boolean
* - -f
* - --option: type
* - --option: type = default
* - --option: type -o
* - --option: type -o = default
* - -o: type
*
* 使 inline-schema 使 ; ,
* - string, number, boolean
* - [string; number]
* - string[]
* - [string; number][]
*
* @example
* 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, name?: string): CommandSchema {
const schema: CommandSchema = {
name: name ?? '',
params: [],
options: [],
flags: [],
};
const tokens = tokenizeSchema(schemaStr);
if (tokens.length === 0) {
return schema;
}
const startIdx = name !== undefined ? 0 : 1;
schema.name = name ?? tokens[0];
let i = startIdx;
while (i < tokens.length) {
const token = tokens[i];
if (token.startsWith('[') && token.endsWith(']')) {
const inner = token.slice(1, -1).trim();
if (inner.startsWith('--')) {
const result = parseOptionToken(inner.slice(2), false);
if (result.isFlag) {
schema.flags.push({ name: result.name, short: result.short });
} else {
schema.options.push({
name: result.name,
short: result.short,
required: false,
defaultValue: result.defaultValue,
schema: result.schema,
});
}
} 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.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;
if (paramContent.includes(':')) {
const colonIndex = paramContent.indexOf(':');
const name = paramContent.slice(0, colonIndex).trim();
const typeStr = paramContent.slice(colonIndex + 1).trim();
try {
parsedSchema = defineSchema(typeStr);
} catch (e) {
// 不是有效的 schema
}
paramContent = name;
}
schema.params.push({
name: paramContent,
required: false,
variadic: isVariadic,
schema: parsedSchema,
});
}
i++;
} else if (token.startsWith('--')) {
const result = parseOptionToken(token.slice(2), true);
if (result.isFlag) {
schema.flags.push({ name: result.name, short: result.short });
} else {
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 result = parseOptionToken(token.slice(1), true);
if (result.isFlag) {
schema.flags.push({ name: result.name, short: result.short || result.name });
} else {
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, '');
if (isVariadic) {
paramContent = paramContent.replace(/\.\.\.$/, '');
}
let parsedSchema: ParsedSchema | undefined;
if (paramContent.includes(':')) {
const colonIndex = paramContent.indexOf(':');
const name = paramContent.slice(0, colonIndex).trim();
const typeStr = paramContent.slice(colonIndex + 1).trim();
try {
parsedSchema = defineSchema(typeStr);
} catch (e) {
// 不是有效的 schema
}
paramContent = name;
}
schema.params.push({
name: paramContent,
required: true,
variadic: isVariadic,
schema: parsedSchema,
});
i++;
} else {
i++;
}
}
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]
*/
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 <val> 中的 <val>
// 这种情况应该由选项处理逻辑处理,不作为独立参数
return true;
}
/**
* schema tokens
* [...args] [--flag]
* <param> <param: type>
*/
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 === ']') {
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 = validateCommandCore(command, schema);
if (errors.length > 0) {
return { valid: false, errors };
}
return { valid: true };
}
/**
*
*/
function validateCommandCore(command: Command, schema: CommandSchema): 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}` : ''}`);
}
}
return errors;
}
/**
* schema
* schema
*
* @param input
* @param schemaStr schema
* @returns
*
* @example
* const result = parseCommandWithSchema(
* 'move [1; 2] region1 --all true',
* 'move <from: [x: string; y: string]> <to: string> [--all: boolean]'
* );
* // result.command.params[0] = ['1', '2'] (已解析为元组)
* // result.command.options.all = true (已解析为布尔值)
*/
export function parseCommandWithSchema(
input: string,
schemaStr: string
): { command: Command; valid: true } | { command: Command; valid: false; errors: string[] } {
const schema = parseCommandSchema(schemaStr);
const command = parseCommand(input);
const errors = validateCommandCore(command, schema);
if (errors.length > 0) {
return { command, valid: false, errors };
}
const parseErrors: string[] = [];
const parsedParams: unknown[] = [];
for (let i = 0; i < command.params.length; i++) {
const paramValue = command.params[i];
const paramSchema = schema.params[i]?.schema;
if (paramSchema) {
try {
const parsed = typeof paramValue === 'string'
? paramSchema.parse(paramValue)
: paramValue;
parsedParams.push(parsed);
} catch (e) {
const err = e as ParseError;
parseErrors.push(`参数 "${schema.params[i]?.name}" 解析失败:${err.message}`);
}
} else {
parsedParams.push(paramValue);
}
}
const parsedOptions: Record<string, unknown> = { ...command.options };
for (const [key, value] of Object.entries(command.options)) {
const optSchema = schema.options.find(o => o.name === key || o.short === key);
if (optSchema?.schema && typeof value === 'string') {
try {
parsedOptions[key] = optSchema.schema.parse(value);
} catch (e) {
const err = e as ParseError;
parseErrors.push(`选项 "--${key}" 解析失败:${err.message}`);
}
}
}
if (parseErrors.length > 0) {
return { command: { ...command, params: parsedParams, options: parsedOptions }, valid: false, errors: parseErrors };
}
return {
command: { ...command, params: parsedParams, options: parsedOptions },
valid: true,
};
}
export { parseCommand } from './command/command-parse.js';
export { parseCommandSchema } from './command/schema-parse.js';
export { validateCommand, parseCommandWithSchema } from './command/command-validate.js';
export type {
Command,
CommandParamSchema,
CommandOptionSchema,
CommandFlagSchema,
CommandSchema,
} from './command/types.js';

View File

@ -0,0 +1,119 @@
import type { Command } from './types.js';
export function parseCommand(input: string): Command {
const tokens = tokenize(input);
if (tokens.length === 0) {
return { name: '', flags: {}, options: {}, params: [] };
}
const name = tokens[0];
const params: unknown[] = [];
const flags: Record<string, true> = {};
const options: Record<string, unknown> = {};
let i = 1;
while (i < tokens.length) {
const token = tokens[i];
if (token.startsWith('--') && !/^-?\d+$/.test(token)) {
const key = token.slice(2);
const nextToken = tokens[i + 1];
if (nextToken && (!nextToken.startsWith('-') || /^-\d+$/.test(nextToken))) {
options[key] = nextToken;
i += 2;
} else {
flags[key] = true;
i++;
}
} else if (token.startsWith('-') && token.length > 1 && !/^-?\d+$/.test(token)) {
const key = token.slice(1);
const nextToken = tokens[i + 1];
if (nextToken && (!nextToken.startsWith('-') || /^-\d+$/.test(nextToken))) {
options[key] = nextToken;
i += 2;
} else {
flags[key] = true;
i++;
}
} else {
params.push(token);
i++;
}
}
return { name, flags, options, params };
}
function tokenize(input: string): string[] {
const tokens: string[] = [];
let current = '';
let inQuote: string | null = null;
let inBracket = false;
let bracketDepth = 0;
let escaped = false;
let i = 0;
while (i < input.length) {
const char = input[i];
if (escaped) {
current += char;
escaped = false;
} else if (char === '\\') {
escaped = true;
} else if (inQuote) {
if (char === inQuote) {
inQuote = null;
} else {
current += char;
}
} else if (char === '"' || 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)) {
if (inBracket) {
current += char;
} else if (current.length > 0) {
tokens.push(current);
current = '';
}
} else {
current += char;
}
i++;
}
if (current.length > 0) {
tokens.push(current);
}
return tokens;
}

View File

@ -0,0 +1,103 @@
import { type ParseError } from 'inline-schema';
import type { Command, CommandSchema } from './types.js';
import { parseCommand } from './command-parse.js';
import { parseCommandSchema } from './schema-parse.js';
export function validateCommand(
command: Command,
schema: CommandSchema
): { valid: true } | { valid: false; errors: string[] } {
const errors = validateCommandCore(command, schema);
if (errors.length > 0) {
return { valid: false, errors };
}
return { valid: true };
}
function validateCommandCore(command: Command, schema: CommandSchema): 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}` : ''}`);
}
}
return errors;
}
export function parseCommandWithSchema(
input: string,
schemaStr: string
): { command: Command; valid: true } | { command: Command; valid: false; errors: string[] } {
const schema = parseCommandSchema(schemaStr);
const command = parseCommand(input);
const errors = validateCommandCore(command, schema);
if (errors.length > 0) {
return { command, valid: false, errors };
}
const parseErrors: string[] = [];
const parsedParams: unknown[] = [];
for (let i = 0; i < command.params.length; i++) {
const paramValue = command.params[i];
const paramSchema = schema.params[i]?.schema;
if (paramSchema) {
try {
const parsed = typeof paramValue === 'string'
? paramSchema.parse(paramValue)
: paramValue;
parsedParams.push(parsed);
} catch (e) {
const err = e as ParseError;
parseErrors.push(`参数 "${schema.params[i]?.name}" 解析失败:${err.message}`);
}
} else {
parsedParams.push(paramValue);
}
}
const parsedOptions: Record<string, unknown> = { ...command.options };
for (const [key, value] of Object.entries(command.options)) {
const optSchema = schema.options.find(o => o.name === key || o.short === key);
if (optSchema?.schema && typeof value === 'string') {
try {
parsedOptions[key] = optSchema.schema.parse(value);
} catch (e) {
const err = e as ParseError;
parseErrors.push(`选项 "--${key}" 解析失败:${err.message}`);
}
}
}
if (parseErrors.length > 0) {
return { command: { ...command, params: parsedParams, options: parsedOptions }, valid: false, errors: parseErrors };
}
return {
command: { ...command, params: parsedParams, options: parsedOptions },
valid: true,
};
}

View File

@ -0,0 +1,264 @@
import { defineSchema, type ParsedSchema } from 'inline-schema';
import type { CommandSchema, CommandParamSchema, CommandOptionSchema, CommandFlagSchema, ParsedOptionResult } from './types.js';
export function parseCommandSchema(schemaStr: string, name?: string): CommandSchema {
const schema: CommandSchema = {
name: name ?? '',
params: [],
options: [],
flags: [],
};
const tokens = tokenizeSchema(schemaStr);
if (tokens.length === 0) {
return schema;
}
const startIdx = name !== undefined ? 0 : 1;
schema.name = name ?? tokens[0];
let i = startIdx;
while (i < tokens.length) {
const token = tokens[i];
if (token.startsWith('[') && token.endsWith(']')) {
const inner = token.slice(1, -1).trim();
if (inner.startsWith('--')) {
const result = parseOptionToken(inner.slice(2), false);
if (result.isFlag) {
schema.flags.push({ name: result.name, short: result.short });
} else {
schema.options.push({
name: result.name,
short: result.short,
required: false,
defaultValue: result.defaultValue,
schema: result.schema,
});
}
} 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.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;
if (paramContent.includes(':')) {
const colonIdx = paramContent.indexOf(':');
const name = paramContent.slice(0, colonIdx).trim();
const typeStr = paramContent.slice(colonIdx + 1).trim();
try {
parsedSchema = defineSchema(typeStr);
} catch (e) {
// 不是有效的 schema
}
paramContent = name;
}
schema.params.push({
name: paramContent,
required: false,
variadic: isVariadic,
schema: parsedSchema,
});
}
i++;
} else if (token.startsWith('--')) {
const result = parseOptionToken(token.slice(2), true);
if (result.isFlag) {
schema.flags.push({ name: result.name, short: result.short });
} else {
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 result = parseOptionToken(token.slice(1), true);
if (result.isFlag) {
schema.flags.push({ name: result.name, short: result.short || result.name });
} else {
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, '');
if (isVariadic) {
paramContent = paramContent.replace(/\.\.\.$/, '');
}
let parsedSchema: ParsedSchema | undefined;
if (paramContent.includes(':')) {
const colonIdx = paramContent.indexOf(':');
const name = paramContent.slice(0, colonIdx).trim();
const typeStr = paramContent.slice(colonIdx + 1).trim();
try {
parsedSchema = defineSchema(typeStr);
} catch (e) {
// 不是有效的 schema
}
paramContent = name;
}
schema.params.push({
name: paramContent,
required: true,
variadic: isVariadic,
schema: parsedSchema,
});
i++;
} else {
i++;
}
}
return schema;
}
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 };
}
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 === ']') {
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;
}

View File

@ -0,0 +1,43 @@
import { type ParsedSchema } from 'inline-schema';
export type Command = {
name: string;
flags: Record<string, true>;
options: Record<string, unknown>;
params: unknown[];
}
export type CommandParamSchema = {
name: string;
required: boolean;
variadic: boolean;
schema?: ParsedSchema;
}
export type CommandOptionSchema = {
name: string;
short?: string;
required: boolean;
defaultValue?: unknown;
schema?: ParsedSchema;
}
export type CommandFlagSchema = {
name: string;
short?: string;
}
export type CommandSchema = {
name: string;
params: CommandParamSchema[];
options: CommandOptionSchema[];
flags: CommandFlagSchema[];
}
export interface ParsedOptionResult {
name: string;
short?: string;
isFlag: boolean;
schema?: ParsedSchema;
defaultValue?: unknown;
}