In my Python application, I need to write a regular expression that matches a C++ for
or while
loop that has been terminated with a semi-colon (;
). For example, it should match this:
for (int i = 0; i < 10; i++);
... but not this:
for (int i = 0; i < 10; i++)
This looks trivial at first glance, until you realise that the text between the opening and closing parenthesis may contain other parenthesis, for example:
for (int i = funcA(); i < funcB(); i++);
I'm using the python.re module. Right now my regular expression looks like this (I've left my comments in so you can understand it easier):
# match any line that begins with a "for" or "while" statement:
^\s*(for|while)\s*
\( # match the initial opening parenthesis
# Now make a named group 'balanced' which matches a balanced substring.
(?P<balanced>
# A balanced substring is either something that is not a parenthesis:
[^()]
| # …or a parenthesised string:
\( # A parenthesised string begins with an opening parenthesis
(?P=balanced)* # …followed by a sequence of balanced substrings
\) # …and ends with a closing parenthesis
)* # Look for a sequence of balanced substrings
\) # Finally, the outer closing parenthesis.
# must end with a semi-colon to match:
\s*;\s*
This works perfectly for all the above cases, but it breaks as soon as you try and make the third part of the for loop contain a function, like so:
for (int i = 0; i < 10; doSomethingTo(i));
I think it breaks because as soon as you put some text between the opening and closing parenthesis, the "balanced" group matches that contained text, and thus the (?P=balanced)
part doesn't work any more since it won't match (due to the fact that the text inside the parenthesis is different).
In my Python code I'm using the VERBOSE and MULTILINE flags, and creating the regular expression like so:
REGEX_STR = r"""# match any line that begins with a "for" or "while" statement:
^\s*(for|while)\s*
\( # match the initial opening parenthesis
# Now make a named group 'balanced' which matches
# a balanced substring.
(?P<balanced>
# A balanced substring is either something that is not a parenthesis:
[^()]
| # …or a parenthesised string:
\( # A parenthesised string begins with an opening parenthesis
(?P=balanced)* # …followed by a sequence of balanced substrings
\) # …and ends with a closing parenthesis
)* # Look for a sequence of balanced substrings
\) # Finally, the outer closing parenthesis.
# must end with a semi-colon to match:
\s*;\s*"""
REGEX_OBJ = re.compile(REGEX_STR, re.MULTILINE| re.VERBOSE)
Can anyone suggest an improvement to this regular expression? It's getting too complicated for me to get my head around.
Another thought that ignores parentheses and treats the
for
as a construct holding three semicolon-delimited values:This option works even when split over multiple lines (once MULTILINE enabled), but assumes that
for ( ... ; ... ; ... )
is the only valid construct, so wouldn't work with afor ( x in y )
construct, or other deviations.Also assumes that there are no functions containing semi-colons as arguments, such as:
Whether this is a likely case depends on what you're actually doing this for.
Greg is absolutely correct. This kind of parsing cannot be done with regular expressions. I suppose it is possible to build some horrendous monstrosity that would work for many cases, but then you'll just run across something that does.
You really need to use more traditional parsing techniques. For example, its pretty simple to write a recursive decent parser to do what you need.
This is the kind of thing you really shouldn't do with a regular expression. Just parse the string one character at a time, keeping track of opening/closing parentheses.
If this is all you're looking for, you definitely don't need a full-blown C++ grammar lexer/parser. If you want practice, you can write a little recursive-decent parser, but even that's a bit much for just matching parentheses.
I wouldn't even pay attention to the contents of the parens.
Just match any line that starts with
for
and ends with semi-colon:Unless you've got
for
statements split over multiple lines, that will work fine?You could write a little, very simple routine that does it, without using a regular expression:
pos
so that is points to just before the opening bracket after yourfor
orwhile
.openBr
to0
.pos
, reading the characters at the respective positions, and incrementopenBr
when you see an opening bracket, and decrement it when you see a closing bracket. That will increment it once at the beginning, for the first opening bracket in "for (
", increment and decrement some more for some brackets in between, and set it back to0
when yourfor
bracket closes.openBr
is0
again.The stopping positon is your closing bracket of
for(...)
. Now you can check if there is a semicolon following or not.I don't know that regex would handle something like that very well. Try something like this