refactor: break up command.ts
This commit is contained in:
parent
ad0d349090
commit
4761806a02
|
|
@ -1,662 +1,10 @@
|
||||||
import { defineSchema, type ParsedSchema, ParseError } from 'inline-schema';
|
export { parseCommand } from './command/command-parse.js';
|
||||||
|
export { parseCommandSchema } from './command/schema-parse.js';
|
||||||
export type Command = {
|
export { validateCommand, parseCommandWithSchema } from './command/command-validate.js';
|
||||||
name: string;
|
export type {
|
||||||
flags: Record<string, true>;
|
Command,
|
||||||
options: Record<string, unknown>;
|
CommandParamSchema,
|
||||||
params: unknown[];
|
CommandOptionSchema,
|
||||||
}
|
CommandFlagSchema,
|
||||||
|
CommandSchema,
|
||||||
/**
|
} from './command/types.js';
|
||||||
* 命令参数 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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue