mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-14 09:01:33 +00:00
fix(cursor): harden shell search pattern parsing (#2938)
Keep unquoted Windows backslashes so rg.exe paths still parse, skip pattern-file operands, and treat grep -r as recursive rather than a valued replace flag. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
3f82e46ca6
commit
beca63d402
2 changed files with 111 additions and 11 deletions
|
|
@ -126,7 +126,13 @@ function tokenizeShellWords(command) {
|
|||
}
|
||||
|
||||
if (char === '\\') {
|
||||
escaped = true;
|
||||
const next = command[index + 1];
|
||||
if (next === undefined || /\s/.test(next) || next === "'" || next === '"' || next === '\\') {
|
||||
escaped = true;
|
||||
} else {
|
||||
current += '\\' + next;
|
||||
index += 1;
|
||||
}
|
||||
hasToken = true;
|
||||
} else if (char === "'" || char === '"') {
|
||||
quote = char;
|
||||
|
|
@ -153,22 +159,55 @@ function parseRgGrepPattern(cmd) {
|
|||
let skipNextAsPattern = false;
|
||||
let endOfOptions = false;
|
||||
let explicitPatternSeen = false;
|
||||
let patternFileSeen = false;
|
||||
const flagsWithValues = new Set([
|
||||
'-e',
|
||||
'-f',
|
||||
'--file',
|
||||
'-m',
|
||||
'--max-count',
|
||||
'-A',
|
||||
'-B',
|
||||
'-C',
|
||||
'-g',
|
||||
'--glob',
|
||||
'--iglob',
|
||||
'-t',
|
||||
'--type',
|
||||
'--include',
|
||||
'--exclude',
|
||||
'--encoding',
|
||||
'--path',
|
||||
]);
|
||||
const rgValueFlags = new Set(['-r', '--replace']);
|
||||
const patternFlags = new Set(['-e', '--regexp']);
|
||||
const connectors = new Set(['&&', '||', ';', '|', '&']);
|
||||
const wrappers = new Set([
|
||||
'npx',
|
||||
'bunx',
|
||||
'pnpm',
|
||||
'yarn',
|
||||
'npm',
|
||||
'sudo',
|
||||
'env',
|
||||
'command',
|
||||
'time',
|
||||
'nice',
|
||||
'xargs',
|
||||
'dlx',
|
||||
'exec',
|
||||
'run',
|
||||
'git',
|
||||
]);
|
||||
const basename = (token) =>
|
||||
token
|
||||
.split(/[\\/]/)
|
||||
.pop()
|
||||
?.replace(/\.(exe|cmd|bat)$/i, '');
|
||||
|
||||
let previousToken;
|
||||
let seenWrapper = false;
|
||||
let searchCommand = null;
|
||||
for (const token of tokens) {
|
||||
if (skipNext) {
|
||||
skipNext = false;
|
||||
|
|
@ -176,18 +215,32 @@ function parseRgGrepPattern(cmd) {
|
|||
skipNextAsPattern = false;
|
||||
if (token.length >= 3) return token;
|
||||
}
|
||||
previousToken = token;
|
||||
continue;
|
||||
}
|
||||
if (!foundCmd) {
|
||||
const commandName = token
|
||||
.split(/[\\/]/)
|
||||
.pop()
|
||||
?.replace(/\.exe$/i, '');
|
||||
if (commandName === 'rg' || commandName === 'grep') foundCmd = true;
|
||||
if (connectors.has(token)) {
|
||||
seenWrapper = false;
|
||||
previousToken = token;
|
||||
continue;
|
||||
}
|
||||
const commandName = basename(token);
|
||||
if (wrappers.has(commandName)) seenWrapper = true;
|
||||
const atCommandPosition =
|
||||
previousToken === undefined ||
|
||||
connectors.has(previousToken) ||
|
||||
wrappers.has(basename(previousToken)) ||
|
||||
seenWrapper;
|
||||
if (atCommandPosition && (commandName === 'rg' || commandName === 'grep')) {
|
||||
foundCmd = true;
|
||||
searchCommand = commandName;
|
||||
}
|
||||
previousToken = token;
|
||||
continue;
|
||||
}
|
||||
previousToken = token;
|
||||
if (endOfOptions) {
|
||||
if (explicitPatternSeen) continue;
|
||||
if (explicitPatternSeen || patternFileSeen) continue;
|
||||
return token.length >= 3 ? token : null;
|
||||
}
|
||||
if (token === '--') {
|
||||
|
|
@ -195,20 +248,39 @@ function parseRgGrepPattern(cmd) {
|
|||
continue;
|
||||
}
|
||||
if (token.startsWith('-')) {
|
||||
const attachedPattern = token.match(/^--regexp=(.+)$/) || token.match(/^-e(.+)$/);
|
||||
if (token === '-f' || token === '--file') {
|
||||
skipNext = true;
|
||||
patternFileSeen = true;
|
||||
continue;
|
||||
}
|
||||
if (token.startsWith('--file=')) {
|
||||
patternFileSeen = true;
|
||||
continue;
|
||||
}
|
||||
if (token.startsWith('--regexp=')) {
|
||||
explicitPatternSeen = true;
|
||||
const value = token.slice('--regexp='.length);
|
||||
if (value.length >= 3) return value;
|
||||
continue;
|
||||
}
|
||||
const attachedPattern = token.match(/^-e(.+)$/);
|
||||
if (attachedPattern) {
|
||||
explicitPatternSeen = true;
|
||||
if (attachedPattern[1].length >= 3) return attachedPattern[1];
|
||||
continue;
|
||||
}
|
||||
if (flagsWithValues.has(token) || patternFlags.has(token)) {
|
||||
if (
|
||||
flagsWithValues.has(token) ||
|
||||
patternFlags.has(token) ||
|
||||
(searchCommand === 'rg' && rgValueFlags.has(token))
|
||||
) {
|
||||
skipNext = true;
|
||||
skipNextAsPattern = patternFlags.has(token);
|
||||
if (skipNextAsPattern) explicitPatternSeen = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (explicitPatternSeen) continue;
|
||||
if (explicitPatternSeen || patternFileSeen) continue;
|
||||
return token.length >= 3 ? token : null;
|
||||
}
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -57,8 +57,9 @@ const CURSOR_HOOKS_JSON = path.resolve(
|
|||
);
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { parseRgGrepPattern } = require(CURSOR_HOOK) as {
|
||||
const { parseRgGrepPattern, tokenizeShellWords } = require(CURSOR_HOOK) as {
|
||||
parseRgGrepPattern: (command: string) => string | null;
|
||||
tokenizeShellWords: (command: string) => string[];
|
||||
};
|
||||
|
||||
// ─── Cursor-specific output parser ──────────────────────────────────
|
||||
|
|
@ -571,10 +572,31 @@ describe('Shell quoted-pattern parser', () => {
|
|||
['rg --regexp=x --regexp=LongPattern src/', 'LongPattern'],
|
||||
['/usr/bin/rg -- "User Service" src/', 'User Service'],
|
||||
['rg -- -error src/', '-error'],
|
||||
[String.raw`C:\Users\me\bin\rg.exe UserService src/`, 'UserService'],
|
||||
['rg.exe "validateUser" src/', 'validateUser'],
|
||||
['grep.cmd -e LongPattern src/', 'LongPattern'],
|
||||
['cd grep && rg LongPattern src/', 'LongPattern'],
|
||||
['npx rg "User Service" src/', 'User Service'],
|
||||
['npx --yes rg UserService src/', 'UserService'],
|
||||
['rg --max-count 100 UserService src/', 'UserService'],
|
||||
['grep -r UserService src/', 'UserService'],
|
||||
['rg --replace x UserService src/', 'UserService'],
|
||||
['git grep UserService src/', 'UserService'],
|
||||
])('extracts %j from %j', (command, expected) => {
|
||||
expect(parseRgGrepPattern(command)).toBe(expected);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['rg --regexp= src/'],
|
||||
['rg --regexp="" src/'],
|
||||
['rg -e x -- LongPattern src/'],
|
||||
['rg -f patterns.txt src/'],
|
||||
['rg --file=patterns.txt src/'],
|
||||
['rg -eab src/'],
|
||||
])('extracts no pattern from %j', (command) => {
|
||||
expect(parseRgGrepPattern(command)).toBeNull();
|
||||
});
|
||||
|
||||
it('does not treat a path after a short explicit pattern as the pattern', () => {
|
||||
expect(parseRgGrepPattern('rg -e x src/')).toBeNull();
|
||||
});
|
||||
|
|
@ -582,6 +604,12 @@ describe('Shell quoted-pattern parser', () => {
|
|||
it('keeps single-token quoted patterns intact', () => {
|
||||
expect(parseRgGrepPattern('rg "validateUser"')).toBe('validateUser');
|
||||
});
|
||||
|
||||
it('keeps backslashes in unquoted Windows paths but honours escaped spaces', () => {
|
||||
expect(tokenizeShellWords(String.raw`C:\foo\bar`)).toEqual([String.raw`C:\foo\bar`]);
|
||||
expect(tokenizeShellWords('User\\ Service')).toEqual(['User Service']);
|
||||
expect(tokenizeShellWords('trailing\\')).toEqual(['trailing\\']);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Install docs ─────────────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue