#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
# Copyright(c) 2025: Mauro Carvalho Chehab <mchehab@kernel.org>.
"""
Regular expression ancillary classes.
Those help caching regular expressions
and do matching
for kernel-doc.
"""
import re
# Local cache for regular expressions
re_cache = {}
class KernRe:
"""
Helper
class to simplify regex declaration
and usage,
It calls re.compile
for a given pattern. It also allows adding
regular expressions
and define sub at
class init time.
Regular expressions can be cached via an argument, helping to speedup
searches.
"""
def _add_regex(self, string, flags):
"""
Adds a new regex
or re-use it
from the cache.
"""
self.regex = re_cache.get(string,
None)
if not self.regex:
self.regex = re.compile(string, flags=flags)
if self.cache:
re_cache[string] = self.regex
def __init__(self, string, cache=
True, flags=0):
"""
Compile a regular expression
and initialize internal vars.
"""
self.cache = cache
self.last_match =
None
self._add_regex(string, flags)
def __str__(self):
"""
Return the regular expression pattern.
"""
return self.regex.pattern
def __add__(self, other):
"""
Allows adding two regular expressions into one.
"""
return KernRe(str(self) + str(other), cache=self.cache
or other.cache,
flags=self.regex.flags | other.regex.flags)
def match(self, string):
"""
Handles a re.match storing its results
"""
self.last_match = self.regex.match(string)
return self.last_match
def search(self, string):
"""
Handles a re.search storing its results
"""
self.last_match = self.regex.search(string)
return self.last_match
def findall(self, string):
"""
Alias to re.findall
"""
return self.regex.findall(string)
def split(self, string):
"""
Alias to re.split
"""
return self.regex.split(string)
def sub(self, sub, string, count=0):
"""
Alias to re.sub
"""
return self.regex.sub(sub, string, count=count)
def group(self, num):
"""
Returns the group results of the last match
"""
return self.last_match.group(num)
class NestedMatch:
"""
Finding nested delimiters
is hard
with regular expressions. It
is
even harder on Python
with its normal re module,
as there are several
advanced regular expressions that are missing.
This
is the case of this pattern:
'\\bSTRUCT_GROUP(\\(((?:(?>[^)(]+)|(?1))*)\\))[^;]*;'
which
is used to properly match open/close parenthesis of the
string search STRUCT_GROUP(),
Add a
class that counts pairs of delimiters, using it to match
and
replace nested expressions.
The original approach was suggested by:
https://stackoverflow.com/questions/5454322/python-how-to-match-nested-parentheses-with-regex
Although I re-implemented it to make it more generic
and match 3 types
of delimiters. The logic checks
if delimiters are paired.
If not, it
will ignore the search string.
"""
# TODO: make NestedMatch handle multiple match groups
#
# Right now, regular expressions to match it are defined only up to
# the start delimiter, e.g.:
#
# \bSTRUCT_GROUP\(
#
# is similar to: STRUCT_GROUP\((.*)\)
# except that the content inside the match group is delimiter's aligned.
#
# The content inside parenthesis are converted into a single replace
# group (e.g. r`\1').
#
# It would be nice to change such definition to support multiple
# match groups, allowing a regex equivalent to.
#
# FOO\((.*), (.*), (.*)\)
#
# it is probably easier to define it not as a regular expression, but
# with some lexical definition like:
#
# FOO(arg1, arg2, arg3)
DELIMITER_PAIRS = {
'{':
'}',
'(':
')',
'[':
']',
}
RE_DELIM = re.compile(r
'[\{\}\[\]\(\)]')
def _search(self, regex, line):
"""
Finds paired blocks
for a regex that ends
with a delimiter.
The suggestion of using finditer to match pairs came
from:
https://stackoverflow.com/questions/5454322/python-how-to-match-nested-parentheses-with-regex
but I ended using a different implementation to align all three types
of delimiters
and seek
for an initial regular expression.
The algorithm seeks
for open/close paired delimiters
and place them
into a stack, yielding a start/stop position of each match when the
stack
is zeroed.
The algorithm shoud work fine
for properly paired lines, but will
silently ignore end delimiters that preceeds an start delimiter.
This should be OK
for kernel-doc parser,
as unaligned delimiters
would cause compilation errors. So, we don
't need to rise exceptions
to cover such issues.
"""
stack = []
for match_re
in regex.finditer(line):
start = match_re.start()
offset = match_re.end()
d = line[offset - 1]
if d
not in self.DELIMITER_PAIRS:
continue
end = self.DELIMITER_PAIRS[d]
stack.append(end)
for match
in self.RE_DELIM.finditer(line[offset:]):
pos = match.start() + offset
d = line[pos]
if d
in self.DELIMITER_PAIRS:
end = self.DELIMITER_PAIRS[d]
stack.append(end)
continue
# Does the end delimiter match what it is expected?
if stack
and d == stack[-1]:
stack.pop()
if not stack:
yield start, offset, pos + 1
break
def search(self, regex, line):
"""
This
is similar to re.search:
It matches a regex that it
is followed by a delimiter,
returning occurrences only
if all delimiters are paired.
"""
for t
in self._search(regex, line):
yield line[t[0]:t[2]]
def sub(self, regex, sub, line, count=0):
"""
This
is similar to re.sub:
It matches a regex that it
is followed by a delimiter,
replacing occurrences only
if all delimiters are paired.
if r
'\1' is used, it works just like re: it places there the
matched paired data
with the delimiter stripped.
If count
is different than zero, it will replace at most count
items.
"""
out =
""
cur_pos = 0
n = 0
for start, end, pos
in self._search(regex, line):
out += line[cur_pos:start]
# Value, ignoring start/end delimiters
value = line[end:pos - 1]
# replaces \1 at the sub string, if \1 is used there
new_sub = sub
new_sub = new_sub.replace(r
'\1', value)
out += new_sub
# Drop end ';' if any
if line[pos] ==
';':
pos += 1
cur_pos = pos
n += 1
if count
and count >= n:
break
# Append the remaining string
l = len(line)
out += line[cur_pos:l]
return out