RegEx in PHP: pattern matching outside of non-exclusive quotes
I am writing a method to remove specific data from a SQL query string, and I need the regex to match any word inside the curly braces ONLY when it appears outside the single quotes. I also need it to account for the possibility of escaping (preceded by a backslash) as well as escaped backslashes.
In the following examples, I need the regex to match {FOO}, not {BAR}:
blah blah {FOO} blah 'I\'m typing {BAR} here with an escaped backslash \\'
blah blah {FOO} 'Three backslashes {BAR} and an escaped quote \\\\\\\' here {BAR}'
I am using preg_match in PHP to get a word in curly braces (FOO in this case). Here's the regex string I have so far:
$regex = '/' .
// Match the word in braces
'\{(\w+)\}' .
// Only if it is followed by an even number of single-quotes
'(?=(?:[^\']*\'[^\']*\')*[^\']*$)' .
// The end
'/';
My logic is that since the only thing I am handling is a legitimate SQL string (besides the added curly brace), if the set of curly braces is followed by an even number of unexperienced quotes, then it must be outside the quotes.
The regex I provided is 100% successful EXCEPT for accounting for hidden quotes. I just need to make sure there is no odd number of backslashes before the quoted match, but for the life of me, I can't pipe that to RegEx. Any members?
a source to share
The way to deal with escaped quotes and backslashes is to consume them in matched pairs.
(?=(?:(?:(?:[^\'\\]++|\\.)*+\'){2})*+(?:[^\'\\]++|\\.)*+$)
In other words, when you look at the next quote, you are missing any pair of characters that start with a backslash. This takes care of all hidden quotes and avoids the backslash. This lookahead will allow escaped characters outside of quoted sections to be escaped, which is probably not necessary, but it probably won't hurt either.
ps, Note the liberal use of possessive quantifiers ( *+
and ++
); without them, you may have performance issues, especially if the target rows are large. Also, if the lines may contain line breaks, you may need to do DOTALL matching (otherwise, "singleline" or "/ s").
However, I agree with mmyers: if you try to parse SQL you will run into problems that regular expressions cannot handle at all. Of everything that has a regular expression, SQL is one of the worst.
a source to share
If you really want to use regular expressions for this, I would do it in two steps:
-
Separate lines from lines without
preg_split
:$re = "('(?:[^\\\\']+|\\\\(\\\\\\\\)*.)*')"; $parts = preg_split('/'.$re.'/', $str, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
-
Replace whatever is on the lines:
foreach ($parts as $key => $val) { if (preg_match('/^'.$re.'$/', $val)) { $parts[$key] = preg_replace('/\{([^}]*)}/', '$1', $val); } }
But a real parser will probably do better as this approach is not that efficient.
a source to share