diff --git a/src/utils/command.ts b/src/utils/command.ts index 2ac6f42..66e5d1a 100644 --- a/src/utils/command.ts +++ b/src/utils/command.ts @@ -8,14 +8,18 @@ /** * 解析命令行输入字符串为 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 = input.trim().split(/\s+/).filter(Boolean); - + const tokens = tokenize(input); + if (tokens.length === 0) { return { name: '', flags: {}, options: {}, params: [] }; } @@ -33,7 +37,7 @@ export function parseCommand(input: string): Command { // 长格式标志或选项:--flag 或 --option value const key = token.slice(2); const nextToken = tokens[i + 1]; - + // 如果下一个 token 存在且不以 - 开头(或者是负数),则是选项值 if (nextToken && (!nextToken.startsWith('-') || /^-\d+$/.test(nextToken))) { options[key] = nextToken; @@ -47,7 +51,7 @@ export function parseCommand(input: string): Command { // 短格式标志或选项:-f 或 -o value(但不匹配负数) const key = token.slice(1); const nextToken = tokens[i + 1]; - + // 如果下一个 token 存在且不以 - 开头(或者是负数),则是选项值 if (nextToken && (!nextToken.startsWith('-') || /^-\d+$/.test(nextToken))) { options[key] = nextToken; @@ -65,4 +69,57 @@ export function parseCommand(input: string): Command { } return { name, flags, options, params }; -} \ No newline at end of file +} + +/** + * 将输入字符串分解为 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; +} diff --git a/tests/utils/command.test.ts b/tests/utils/command.test.ts index d3acb47..3247887 100644 --- a/tests/utils/command.test.ts +++ b/tests/utils/command.test.ts @@ -111,4 +111,64 @@ describe('parseCommand', () => { 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"] + }); + }); });