fix: command parsing quotes

This commit is contained in:
hyper 2026-04-01 17:40:28 +08:00
parent 8d778f9867
commit 284251ddf2
2 changed files with 123 additions and 6 deletions

View File

@ -8,14 +8,18 @@
/** /**
* Command * Command
* commandName [params...] [--flags...] [-o value...] * commandName [params...] [--flags...] [-o value...]
* * (') (")
* 使 (\)
*
* @example * @example
* parseCommand("move meeple1 region1 --force -x 10") * parseCommand("move meeple1 region1 --force -x 10")
* // returns { name: "move", params: ["meeple1", "region1"], flags: { force: true }, options: { 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 { export function parseCommand(input: string): Command {
const tokens = input.trim().split(/\s+/).filter(Boolean); const tokens = tokenize(input);
if (tokens.length === 0) { if (tokens.length === 0) {
return { name: '', flags: {}, options: {}, params: [] }; return { name: '', flags: {}, options: {}, params: [] };
} }
@ -33,7 +37,7 @@ export function parseCommand(input: string): Command {
// 长格式标志或选项:--flag 或 --option value // 长格式标志或选项:--flag 或 --option value
const key = token.slice(2); const key = token.slice(2);
const nextToken = tokens[i + 1]; const nextToken = tokens[i + 1];
// 如果下一个 token 存在且不以 - 开头(或者是负数),则是选项值 // 如果下一个 token 存在且不以 - 开头(或者是负数),则是选项值
if (nextToken && (!nextToken.startsWith('-') || /^-\d+$/.test(nextToken))) { if (nextToken && (!nextToken.startsWith('-') || /^-\d+$/.test(nextToken))) {
options[key] = nextToken; options[key] = nextToken;
@ -47,7 +51,7 @@ export function parseCommand(input: string): Command {
// 短格式标志或选项:-f 或 -o value但不匹配负数 // 短格式标志或选项:-f 或 -o value但不匹配负数
const key = token.slice(1); const key = token.slice(1);
const nextToken = tokens[i + 1]; const nextToken = tokens[i + 1];
// 如果下一个 token 存在且不以 - 开头(或者是负数),则是选项值 // 如果下一个 token 存在且不以 - 开头(或者是负数),则是选项值
if (nextToken && (!nextToken.startsWith('-') || /^-\d+$/.test(nextToken))) { if (nextToken && (!nextToken.startsWith('-') || /^-\d+$/.test(nextToken))) {
options[key] = nextToken; options[key] = nextToken;
@ -65,4 +69,57 @@ export function parseCommand(input: string): Command {
} }
return { name, flags, options, params }; return { name, flags, options, params };
} }
/**
* tokens
*/
function tokenize(input: string): string[] {
const tokens: string[] = [];
let current = '';
let inQuote: string | null = null; // ' 或 " 或 null
let escaped = false;
let i = 0;
while (i < input.length) {
const char = input[i];
if (escaped) {
// 转义字符:直接添加到当前 token
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 (/\s/.test(char)) {
// 空白字符
if (current.length > 0) {
tokens.push(current);
current = '';
}
} else {
// 普通字符
current += char;
}
i++;
}
// 处理未闭合的引号
if (current.length > 0) {
tokens.push(current);
}
return tokens;
}

View File

@ -111,4 +111,64 @@ describe('parseCommand', () => {
params: [] params: []
}); });
}); });
it('should parse quoted string with double quotes', () => {
const result = parseCommand('place tile "large castle" --x 5');
expect(result).toEqual({
name: 'place',
flags: {},
options: { x: '5' },
params: ['tile', 'large castle']
});
});
it('should parse quoted string with single quotes', () => {
const result = parseCommand("place tile 'large castle' --x 5");
expect(result).toEqual({
name: 'place',
flags: {},
options: { x: '5' },
params: ['tile', 'large castle']
});
});
it('should handle escaped quotes', () => {
const result = parseCommand('say "hello \\"world\\""');
expect(result).toEqual({
name: 'say',
flags: {},
options: {},
params: ['hello "world"']
});
});
it('should handle escaped backslash', () => {
const result = parseCommand('set path "C:\\\\Users"');
expect(result).toEqual({
name: 'set',
flags: {},
options: {},
params: ['path', 'C:\\Users']
});
});
it('should handle mixed quotes', () => {
const result = parseCommand('cmd "hello world" \'foo bar\' --flag');
expect(result).toEqual({
name: 'cmd',
flags: { flag: true },
options: {},
params: ['hello world', 'foo bar']
});
});
it('should handle quote in middle of argument', () => {
const result = parseCommand('cmd "hello\'s world"');
expect(result).toEqual({
name: 'cmd',
flags: {},
options: {},
params: ["hello's world"]
});
});
}); });