Skip to content

Commit 2ca068f

Browse files
committed
SedRegex: add support for custom delimiters
Via a new "plugins.SedRegex.delimiters" setting. This defaults to all symbols to match the existing behaviour. Fixes #1610.
1 parent eafafbe commit 2ca068f

File tree

4 files changed

+63
-22
lines changed

4 files changed

+63
-22
lines changed

plugins/SedRegex/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ def configure(advanced):
4949

5050
conf.registerChannelValue(SedRegex, 'displayErrors',
5151
registry.Boolean(True, _("""Should errors be displayed?""")))
52+
conf.registerChannelValue(SedRegex, 'delimiters',
53+
registry.String('', _("""List of delimiters to match sed expressions on.
54+
Multiple delimiters can be specified as a single string: e.g. "/@".
55+
If empty, defaults to all symbols.""")))
5256
conf.registerChannelValue(SedRegex, 'boldReplacementText',
5357
registry.Boolean(True, _("""Should the replacement text be bolded?""")))
5458
conf.registerChannelValue(SedRegex, 'enable',

plugins/SedRegex/plugin.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
except ImportError:
4848
_ = lambda x: x
4949

50-
from .sedregex import SED_REGEX
50+
from .sedregex import makeSedRegex
5151

5252
TAG_SEEN = 'SedRegex.seen'
5353
TAG_IS_REGEX = 'SedRegex.isRegex'
@@ -92,7 +92,7 @@ class SedRegex(callbacks.Plugin):
9292
public = True
9393

9494
@staticmethod
95-
def _unpack_sed(expr):
95+
def _unpack_sed(sedRegex, expr):
9696
if '\0' in expr:
9797
raise ValueError('Expression can\'t contain NUL')
9898

@@ -107,7 +107,7 @@ def _unpack_sed(expr):
107107

108108
escaped_expr += c
109109

110-
match = SED_REGEX.search(escaped_expr)
110+
match = sedRegex.search(escaped_expr)
111111

112112
if not match:
113113
return
@@ -145,7 +145,13 @@ def doPrivmsg(self, irc, msg):
145145
# SedRegex was enabled.
146146
msg.tag(TAG_SEEN)
147147

148-
regexMatch = SED_REGEX.match(msg.args[1])
148+
149+
delimiters = self.registryValue('delimiters', msg.channel, irc.network)
150+
if delimiters:
151+
delimiters = re.escape(delimiters)
152+
sedRegex = makeSedRegex(delimiters)
153+
text = msg.args[1]
154+
regexMatch = sedRegex.match(text)
149155
if not regexMatch:
150156
return
151157

@@ -154,7 +160,7 @@ def doPrivmsg(self, irc, msg):
154160
msg.tag(TAG_IS_REGEX)
155161

156162
try:
157-
(pattern, replacement, count, flags) = self._unpack_sed(msg.args[1])
163+
(pattern, replacement, count, flags) = self._unpack_sed(sedRegex, text)
158164
except Exception as e:
159165
self.log.warning(_("SedRegex parser error: %s"), e, exc_info=True)
160166
if self.registryValue('displayErrors', msg.channel, irc.network):
@@ -172,7 +178,7 @@ def doPrivmsg(self, irc, msg):
172178
regex_timeout = self.registryValue('processTimeout')
173179
try:
174180
message = process(self._replacer_process, irc, msg,
175-
target, pattern, replacement, count, iterable,
181+
target, pattern, replacement, count, iterable, sedRegex,
176182
timeout=regex_timeout, pn=self.name(), cn='replacer')
177183
except ProcessTimeoutError:
178184
irc.error(_("Search timed out."))
@@ -187,7 +193,7 @@ def doPrivmsg(self, irc, msg):
187193
else:
188194
irc.reply(message, prefixNick=False)
189195

190-
def _replacer_process(self, irc, msg, target, pattern, replacement, count, messages):
196+
def _replacer_process(self, irc, msg, target, pattern, replacement, count, messages, sedRegex):
191197
for m in messages:
192198
if m.command in ('PRIVMSG', 'NOTICE') and \
193199
ircutils.strEqual(m.args[0], msg.args[0]) and m.tagged('receivedBy') == irc:
@@ -209,7 +215,7 @@ def _replacer_process(self, irc, msg, target, pattern, replacement, count, messa
209215
# so we only need to do this check once per message.
210216
if not m.tagged(TAG_SEEN):
211217
m.tag(TAG_SEEN)
212-
if SED_REGEX.match(m.args[1]):
218+
if sedRegex.match(m.args[1]):
213219
m.tag(TAG_IS_REGEX)
214220
# Ignore messages containing a regexp if ignoreRegex is on.
215221
if self.registryValue('ignoreRegex', msg.channel, irc.network) and m.tagged(TAG_IS_REGEX):

plugins/SedRegex/sedregex.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,29 @@
22

33
import re
44

5-
SED_REGEX = re.compile(
6-
# This part matches an optional nick followed by ":" or ",", used to direct replacement
7-
# at a particular user.
8-
r"^(?:(?P<nick>.+?)[:,] )?"
5+
def makeSedRegex(delimiters=''):
6+
delimiters = delimiters or r"^\w\s"
97

10-
# Match and save the delimiter (any one symbol) as a named group
11-
r"s(?P<delim>[^\w\s])"
8+
return re.compile(
9+
# This part matches an optional nick followed by ":" or ",", used to direct replacement
10+
# at a particular user.
11+
r"^(?:(?P<nick>.+?)[:,] )?"
1212

13-
# Match the pattern to replace, which can be any string up to the first instance of the delimiter
14-
r"(?P<pattern>(?:(?!(?P=delim)).)*)(?P=delim)"
13+
# Match and save the delimiter (any one symbol) as a named group
14+
fr"s(?P<delim>[{delimiters}])"
1515

16-
# Ditto with the replacement
17-
r"(?P<replacement>(?:(?!(?P=delim)).)*)"
16+
# Match the pattern to replace, which can be any string up to the first instance of the
17+
# delimiter
18+
r"(?P<pattern>(?:(?!(?P=delim)).)*)(?P=delim)"
1819

19-
# Optional final delimiter plus flags at the end
20-
r"(?:(?P=delim)(?P<flags>[a-z]*))?"
21-
)
20+
# Ditto with the replacement
21+
r"(?P<replacement>(?:(?!(?P=delim)).)*)"
22+
23+
# Optional final delimiter plus flags at the end
24+
r"(?:(?P=delim)(?P<flags>[a-z]*))?"
25+
)
2226

2327
if __name__ == '__main__':
2428
print("This is the full regex used by the plugin; paste it into your favourite regex tester "
2529
"for debugging:")
26-
print(SED_REGEX)
30+
print(makeSedRegex())

plugins/SedRegex/test.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,33 @@ def testNonSlashSeparator(self):
151151
m = self.getMsg(' ')
152152
self.assertIn('4 * 2 = 8', str(m))
153153

154+
def testCustomDelimiters(self):
155+
with conf.supybot.plugins.sedregex.delimiters.context("@."):
156+
self.feedMsg('test')
157+
self.feedMsg('s@t@b')
158+
m = self.getMsg(' ')
159+
self.assertIn('best', str(m))
160+
161+
self.feedMsg('s.t.w')
162+
m = self.getMsg(' ')
163+
self.assertIn('west', str(m))
164+
165+
# / is not in the delimiters list, so it is ignored
166+
self.getMsg('s/t/r')
167+
for msg in self.irc.state.history:
168+
self.assertNotIn("rest", str(msg))
169+
170+
# These would fail if the delimiters set isn't escaped correctly
171+
with conf.supybot.plugins.sedregex.delimiters.context("]["):
172+
self.feedMsg('test')
173+
self.feedMsg('s]t]f')
174+
m = self.getMsg(' ')
175+
self.assertIn('fest', str(m))
176+
177+
self.feedMsg('s[t[qu[')
178+
m = self.getMsg(' ')
179+
self.assertIn('quest', str(m))
180+
154181
def testWeirdSeparatorsFail(self):
155182
self.feedMsg("can't touch this", frm=self.__class__.other)
156183
# Only symbols are allowed as separators

0 commit comments

Comments
 (0)