I'm trying to extract file paths (Windows/Ubuntu, relative/absolute) from a text document.
The regular expression code below is used check if a word is a file path or not.
It works for most of the cases but fails for one case, where it goes into an infinite loop. Any explanation for this?
import re
path_regex = re.compile(r'^([\.]*)([/]+)(((?![<>:"/\\|?*]).)+((?<![ .])(\\|/))?)*$' , re.I)
text = '/var/lib/jenkins/jobs/abcd-deploy-test-environment-oneccc/workspace/../workspace/abcd-deploy-test-environment.sh'
path_regex.search(text)
Indeed there is a problem.
You have overlayed subexpressions mixed with spurious quantifiers.
modified for required parts between slashes
It is easily fixed using this ^([\.]*)([/]+)((?:[^<>:"/\\|?*.\r\n]|\.(?![\\/]))[\\/]?)*$
The idea is to see just what your guarding against.
The guard is that you'd allow forward or back slash if not preceeded by a dot.
So, you have to include the dot in the exclusion class with the \ and /
then qualify them in a separate alternation.
If you do it this way, it will always pass.
^
( [\.]* ) # (1)
( [/]+ ) # (2)
( # (3 start)
(?: # Group start (required between slashes)
[^<>:"/\\|?*.\r\n] # Any character, but exclude these
| # or,
\. # The dot, if not followed by forward or back slash
(?! [\\/] )
) # Group end
[\\/]? # Optional forward or back shash
)* # (3 end)
$
sln gave a good solution to your problem, so I'll try to explain what the problem is.
Welcome to the joys of catastrophic backtracking. The core of your problem is in (((?![<>:"/\\|?*]).)+((?<![ .])(\\|/))?)*. (Now that I've said that, all your problems are solved, right? Easy peasy.)
Assuming you're a bit like me and blinked blankly a couple of times the first time someone said "regex backtracking", we can work through your regex with the shorter input /path./. This is an invalid path according to your regex, but lets us (somewhat) easily walk through the problem.
^([\.]*)([/]+) matches the leading /. This works fine.
For the sake of readability here, I'm going to call the first half of the problematic capture group, ((?![<>:"/\\|?*]).)+, x+, and the second half, ((?<![ .])(\\|/))?, y?. The whole group is (x+y?).
(x+y?)*$ backtracking when matching path./?x+ matches path.y? matches (it matches 0 times, which is fine because of the ?)(x+y?) has now matched once(x+y?) repeats, and fails, since it does not match /. Thus, (x+y?)* has matched path.$ fails, since it does not match /.(x+y?)* can only backtrack into its first iteration, since it only had one iteration.y? can't backtrack, since it matched 0 times.x+ backtracks, to match only path instead of path.x+ matches pathy? matches (x+y?) has now matched once (path)(x+y?) repeats and matches again:
x+ matches .y? matches (x+y?) repeats, and fails since it does not match /. Thus, (x+y?)* has matched path.$ fails, since it does not match /.(x+y?)* can only backtrack into its first iteration, since in its second iteration x+ matched only once and y? matched 0 times.y? in the first iteration matched 0 times, so it can't backtrackx+ can backtrack to only matching pat(x+y?) matches twice: pat, h.; then on the next backtrack we have pat h ., and then pa th., and so on.It takes 478 steps for the engine to determine that /path./ is not matched by your regex. Every additional character in that problematic capture group increases the number of backtracks by a lot, and after a certain point your regex implementation is just going to throw up its hands and give up. sln's solution takes only 49 steps.
The behaviour of a regex engine when backtracking is difficult both to explain and to grasp, especially when limited to Markdown, so I would recommend running your regex through a debugger to visualize what's going on.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With