mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat(cli): Handle Punctuation in @ Command Parsing (#5482)
This commit is contained in:
214
packages/core/src/utils/paths.test.ts
Normal file
214
packages/core/src/utils/paths.test.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { escapePath, unescapePath } from './paths.js';
|
||||
|
||||
describe('escapePath', () => {
|
||||
it('should escape spaces', () => {
|
||||
expect(escapePath('my file.txt')).toBe('my\\ file.txt');
|
||||
});
|
||||
|
||||
it('should escape tabs', () => {
|
||||
expect(escapePath('file\twith\ttabs.txt')).toBe('file\\\twith\\\ttabs.txt');
|
||||
});
|
||||
|
||||
it('should escape parentheses', () => {
|
||||
expect(escapePath('file(1).txt')).toBe('file\\(1\\).txt');
|
||||
});
|
||||
|
||||
it('should escape square brackets', () => {
|
||||
expect(escapePath('file[backup].txt')).toBe('file\\[backup\\].txt');
|
||||
});
|
||||
|
||||
it('should escape curly braces', () => {
|
||||
expect(escapePath('file{temp}.txt')).toBe('file\\{temp\\}.txt');
|
||||
});
|
||||
|
||||
it('should escape semicolons', () => {
|
||||
expect(escapePath('file;name.txt')).toBe('file\\;name.txt');
|
||||
});
|
||||
|
||||
it('should escape ampersands', () => {
|
||||
expect(escapePath('file&name.txt')).toBe('file\\&name.txt');
|
||||
});
|
||||
|
||||
it('should escape pipes', () => {
|
||||
expect(escapePath('file|name.txt')).toBe('file\\|name.txt');
|
||||
});
|
||||
|
||||
it('should escape asterisks', () => {
|
||||
expect(escapePath('file*.txt')).toBe('file\\*.txt');
|
||||
});
|
||||
|
||||
it('should escape question marks', () => {
|
||||
expect(escapePath('file?.txt')).toBe('file\\?.txt');
|
||||
});
|
||||
|
||||
it('should escape dollar signs', () => {
|
||||
expect(escapePath('file$name.txt')).toBe('file\\$name.txt');
|
||||
});
|
||||
|
||||
it('should escape backticks', () => {
|
||||
expect(escapePath('file`name.txt')).toBe('file\\`name.txt');
|
||||
});
|
||||
|
||||
it('should escape single quotes', () => {
|
||||
expect(escapePath("file'name.txt")).toBe("file\\'name.txt");
|
||||
});
|
||||
|
||||
it('should escape double quotes', () => {
|
||||
expect(escapePath('file"name.txt')).toBe('file\\"name.txt');
|
||||
});
|
||||
|
||||
it('should escape hash symbols', () => {
|
||||
expect(escapePath('file#name.txt')).toBe('file\\#name.txt');
|
||||
});
|
||||
|
||||
it('should escape exclamation marks', () => {
|
||||
expect(escapePath('file!name.txt')).toBe('file\\!name.txt');
|
||||
});
|
||||
|
||||
it('should escape tildes', () => {
|
||||
expect(escapePath('file~name.txt')).toBe('file\\~name.txt');
|
||||
});
|
||||
|
||||
it('should escape less than and greater than signs', () => {
|
||||
expect(escapePath('file<name>.txt')).toBe('file\\<name\\>.txt');
|
||||
});
|
||||
|
||||
it('should handle multiple special characters', () => {
|
||||
expect(escapePath('my file (backup) [v1.2].txt')).toBe(
|
||||
'my\\ file\\ \\(backup\\)\\ \\[v1.2\\].txt',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not double-escape already escaped characters', () => {
|
||||
expect(escapePath('my\\ file.txt')).toBe('my\\ file.txt');
|
||||
expect(escapePath('file\\(name\\).txt')).toBe('file\\(name\\).txt');
|
||||
});
|
||||
|
||||
it('should handle escaped backslashes correctly', () => {
|
||||
// Double backslash (escaped backslash) followed by space should escape the space
|
||||
expect(escapePath('path\\\\ file.txt')).toBe('path\\\\\\ file.txt');
|
||||
// Triple backslash (escaped backslash + escaping backslash) followed by space should not double-escape
|
||||
expect(escapePath('path\\\\\\ file.txt')).toBe('path\\\\\\ file.txt');
|
||||
// Quadruple backslash (two escaped backslashes) followed by space should escape the space
|
||||
expect(escapePath('path\\\\\\\\ file.txt')).toBe('path\\\\\\\\\\ file.txt');
|
||||
});
|
||||
|
||||
it('should handle complex escaped backslash scenarios', () => {
|
||||
// Escaped backslash before special character that needs escaping
|
||||
expect(escapePath('file\\\\(test).txt')).toBe('file\\\\\\(test\\).txt');
|
||||
// Multiple escaped backslashes
|
||||
expect(escapePath('path\\\\\\\\with space.txt')).toBe(
|
||||
'path\\\\\\\\with\\ space.txt',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle paths without special characters', () => {
|
||||
expect(escapePath('normalfile.txt')).toBe('normalfile.txt');
|
||||
expect(escapePath('path/to/normalfile.txt')).toBe('path/to/normalfile.txt');
|
||||
});
|
||||
|
||||
it('should handle complex real-world examples', () => {
|
||||
expect(escapePath('My Documents/Project (2024)/file [backup].txt')).toBe(
|
||||
'My\\ Documents/Project\\ \\(2024\\)/file\\ \\[backup\\].txt',
|
||||
);
|
||||
expect(escapePath('file with $special &chars!.txt')).toBe(
|
||||
'file\\ with\\ \\$special\\ \\&chars\\!.txt',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty strings', () => {
|
||||
expect(escapePath('')).toBe('');
|
||||
});
|
||||
|
||||
it('should handle paths with only special characters', () => {
|
||||
expect(escapePath(' ()[]{};&|*?$`\'"#!~<>')).toBe(
|
||||
'\\ \\(\\)\\[\\]\\{\\}\\;\\&\\|\\*\\?\\$\\`\\\'\\"\\#\\!\\~\\<\\>',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unescapePath', () => {
|
||||
it('should unescape spaces', () => {
|
||||
expect(unescapePath('my\\ file.txt')).toBe('my file.txt');
|
||||
});
|
||||
|
||||
it('should unescape tabs', () => {
|
||||
expect(unescapePath('file\\\twith\\\ttabs.txt')).toBe(
|
||||
'file\twith\ttabs.txt',
|
||||
);
|
||||
});
|
||||
|
||||
it('should unescape parentheses', () => {
|
||||
expect(unescapePath('file\\(1\\).txt')).toBe('file(1).txt');
|
||||
});
|
||||
|
||||
it('should unescape square brackets', () => {
|
||||
expect(unescapePath('file\\[backup\\].txt')).toBe('file[backup].txt');
|
||||
});
|
||||
|
||||
it('should unescape curly braces', () => {
|
||||
expect(unescapePath('file\\{temp\\}.txt')).toBe('file{temp}.txt');
|
||||
});
|
||||
|
||||
it('should unescape multiple special characters', () => {
|
||||
expect(unescapePath('my\\ file\\ \\(backup\\)\\ \\[v1.2\\].txt')).toBe(
|
||||
'my file (backup) [v1.2].txt',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle paths without escaped characters', () => {
|
||||
expect(unescapePath('normalfile.txt')).toBe('normalfile.txt');
|
||||
expect(unescapePath('path/to/normalfile.txt')).toBe(
|
||||
'path/to/normalfile.txt',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle all special characters', () => {
|
||||
expect(
|
||||
unescapePath(
|
||||
'\\ \\(\\)\\[\\]\\{\\}\\;\\&\\|\\*\\?\\$\\`\\\'\\"\\#\\!\\~\\<\\>',
|
||||
),
|
||||
).toBe(' ()[]{};&|*?$`\'"#!~<>');
|
||||
});
|
||||
|
||||
it('should be the inverse of escapePath', () => {
|
||||
const testCases = [
|
||||
'my file.txt',
|
||||
'file(1).txt',
|
||||
'file[backup].txt',
|
||||
'My Documents/Project (2024)/file [backup].txt',
|
||||
'file with $special &chars!.txt',
|
||||
' ()[]{};&|*?$`\'"#!~<>',
|
||||
'file\twith\ttabs.txt',
|
||||
];
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
expect(unescapePath(escapePath(testCase))).toBe(testCase);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty strings', () => {
|
||||
expect(unescapePath('')).toBe('');
|
||||
});
|
||||
|
||||
it('should not affect backslashes not followed by special characters', () => {
|
||||
expect(unescapePath('file\\name.txt')).toBe('file\\name.txt');
|
||||
expect(unescapePath('path\\to\\file.txt')).toBe('path\\to\\file.txt');
|
||||
});
|
||||
|
||||
it('should handle escaped backslashes in unescaping', () => {
|
||||
// Should correctly unescape when there are escaped backslashes
|
||||
expect(unescapePath('path\\\\\\ file.txt')).toBe('path\\\\ file.txt');
|
||||
expect(unescapePath('path\\\\\\\\\\ file.txt')).toBe(
|
||||
'path\\\\\\\\ file.txt',
|
||||
);
|
||||
expect(unescapePath('file\\\\\\(test\\).txt')).toBe('file\\\\(test).txt');
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,13 @@ export const GOOGLE_ACCOUNTS_FILENAME = 'google_accounts.json';
|
||||
const TMP_DIR_NAME = 'tmp';
|
||||
const COMMANDS_DIR_NAME = 'commands';
|
||||
|
||||
/**
|
||||
* Special characters that need to be escaped in file paths for shell compatibility.
|
||||
* Includes: spaces, parentheses, brackets, braces, semicolons, ampersands, pipes,
|
||||
* asterisks, question marks, dollar signs, backticks, quotes, hash, and other shell metacharacters.
|
||||
*/
|
||||
export const SHELL_SPECIAL_CHARS = /[ \t()[\]{};|*?$`'"#&<>!~]/;
|
||||
|
||||
/**
|
||||
* Replaces the home directory with a tilde.
|
||||
* @param path - The path to tildeify.
|
||||
@@ -119,26 +126,43 @@ export function makeRelative(
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes spaces in a file path.
|
||||
* Escapes special characters in a file path like macOS terminal does.
|
||||
* Escapes: spaces, parentheses, brackets, braces, semicolons, ampersands, pipes,
|
||||
* asterisks, question marks, dollar signs, backticks, quotes, hash, and other shell metacharacters.
|
||||
*/
|
||||
export function escapePath(filePath: string): string {
|
||||
let result = '';
|
||||
for (let i = 0; i < filePath.length; i++) {
|
||||
// Only escape spaces that are not already escaped.
|
||||
if (filePath[i] === ' ' && (i === 0 || filePath[i - 1] !== '\\')) {
|
||||
result += '\\ ';
|
||||
const char = filePath[i];
|
||||
|
||||
// Count consecutive backslashes before this character
|
||||
let backslashCount = 0;
|
||||
for (let j = i - 1; j >= 0 && filePath[j] === '\\'; j--) {
|
||||
backslashCount++;
|
||||
}
|
||||
|
||||
// Character is already escaped if there's an odd number of backslashes before it
|
||||
const isAlreadyEscaped = backslashCount % 2 === 1;
|
||||
|
||||
// Only escape if not already escaped
|
||||
if (!isAlreadyEscaped && SHELL_SPECIAL_CHARS.test(char)) {
|
||||
result += '\\' + char;
|
||||
} else {
|
||||
result += filePath[i];
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescapes spaces in a file path.
|
||||
* Unescapes special characters in a file path.
|
||||
* Removes backslash escaping from shell metacharacters.
|
||||
*/
|
||||
export function unescapePath(filePath: string): string {
|
||||
return filePath.replace(/\\ /g, ' ');
|
||||
return filePath.replace(
|
||||
new RegExp(`\\\\([${SHELL_SPECIAL_CHARS.source.slice(1, -1)}])`, 'g'),
|
||||
'$1',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user