Skip to content

Commit

Permalink
Backport find escaping fixes etc. from #2589 to 8.0.1
Browse files Browse the repository at this point in the history
  • Loading branch information
dgw committed Oct 14, 2024
1 parent 4badf8a commit b1fa914
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 19 deletions.
50 changes: 31 additions & 19 deletions sopel/builtins/find.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,11 @@ def kick_cleanup(bot, trigger):
[:,]\s+)? # Followed by optional colon/comma and whitespace
s(?P<sep>/) # The literal s and a separator / as group 2
(?P<old> # Group 3 is the thing to find
(?:\\/|[^/])+ # One or more non-slashes or escaped slashes
(?:\\\\|\\/|[^/])+ # One or more non-slashes or escaped slashes
)
/ # The separator again
(?P<new> # Group 4 is what to replace with
(?:\\/|[^/])* # One or more non-slashes or escaped slashes
(?:\\\\|\\/|[^/])* # One or more non-slashes or escaped slashes
)
(?:/ # Optional separator followed by group 5 (flags)
(?P<flags>\S+)
Expand All @@ -136,11 +136,11 @@ def kick_cleanup(bot, trigger):
[:,]\s+)? # Followed by optional colon/comma and whitespace
s(?P<sep>\|) # The literal s and a separator | as group 2
(?P<old> # Group 3 is the thing to find
(?:\\\||[^|])+ # One or more non-pipe or escaped pipe
(?:\\\\|\\\||[^|])+ # One or more non-pipe or escaped pipe
)
\| # The separator again
(?P<new> # Group 4 is what to replace with
(?:\\\||[^|])* # One or more non-pipe or escaped pipe
(?:\\\\|\\\||[^|])* # One or more non-pipe or escaped pipe
)
(?:\| # Optional separator followed by group 5 (flags)
(?P<flags>\S+)
Expand All @@ -161,14 +161,16 @@ def findandreplace(bot, trigger):
return

sep = trigger.group('sep')
old = trigger.group('old').replace('\\%s' % sep, sep)
escape_sequence_pattern = re.compile(r'\\[\\%s]' % sep)

old = escape_sequence_pattern.sub(decode_escape, trigger.group('old'))
new = trigger.group('new')
me = False # /me command
flags = trigger.group('flags') or ''

# only clean/format the new string if it's non-empty
if new:
new = bold(new.replace('\\%s' % sep, sep))
new = escape_sequence_pattern.sub(decode_escape, new)

# If g flag is given, replace all. Otherwise, replace once.
if 'g' in flags:
Expand All @@ -181,39 +183,49 @@ def findandreplace(bot, trigger):
if 'i' in flags:
regex = re.compile(re.escape(old), re.U | re.I)

def repl(s):
return re.sub(regex, new, s, count == 1)
def repl(line, subst):
return re.sub(regex, subst, line, count == 1)
else:
def repl(s):
return s.replace(old, new, count)
def repl(line, subst):
return line.replace(old, subst, count)

# Look back through the user's lines in the channel until you find a line
# where the replacement works
new_phrase = None
new_line = new_display = None
for line in history:
if line.startswith("\x01ACTION"):
me = True # /me command
line = line[8:]
else:
me = False
replaced = repl(line)
replaced = repl(line, new)
if replaced != line: # we are done
new_phrase = replaced
new_line = replaced
new_display = repl(line, bold(new))
break

if not new_phrase:
if not new_line:
return # Didn't find anything

# Save the new "edited" message.
action = (me and '\x01ACTION ') or '' # If /me message, prepend \x01ACTION
history.appendleft(action + new_phrase) # history is in most-recent-first order
history.appendleft(action + new_line) # history is in most-recent-first order

# output
if not me:
new_phrase = 'meant to say: %s' % new_phrase
new_display = 'meant to say: %s' % new_display
if trigger.group(1):
phrase = '%s thinks %s %s' % (trigger.nick, rnick, new_phrase)
msg = '%s thinks %s %s' % (trigger.nick, rnick, new_display)
else:
phrase = '%s %s' % (trigger.nick, new_phrase)
msg = '%s %s' % (trigger.nick, new_display)

bot.say(msg)


bot.say(phrase)
def decode_escape(match):
print("Substituting %s" % match.group(0))
return {
r'\\': '\\',
r'\|': '|',
r'\/': '/',
}[match.group(0)]
107 changes: 107 additions & 0 deletions test/builtins/test_builtins_find.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Tests for Sopel's ``find`` plugin"""
from __future__ import annotations

import pytest

from sopel.formatting import bold
from sopel.tests import rawlist


TMP_CONFIG = """
[core]
owner = Admin
nick = Sopel
enable =
find
host = irc.libera.chat
"""


@pytest.fixture
def bot(botfactory, configfactory):
settings = configfactory('default.ini', TMP_CONFIG)
return botfactory.preloaded(settings, ['find'])


@pytest.fixture
def irc(bot, ircfactory):
return ircfactory(bot)


@pytest.fixture
def user(userfactory):
return userfactory('User')


@pytest.fixture
def other_user(userfactory):
return userfactory('other_user')


@pytest.fixture
def channel():
return '#testing'


REPLACES_THAT_WORK = (
("A simple line.", r"s/line/message/", f"A simple {bold('message')}."),
("An escaped / line.", r"s/\//slash/", f"An escaped {bold('slash')} line."),
("A piped line.", r"s|line|replacement|", f"A piped {bold('replacement')}."),
("An escaped | line.", r"s|\||pipe|", f"An escaped {bold('pipe')} line."),
("An escaped \\ line.", r"s/\\/backslash/", f"An escaped {bold('backslash')} line."),
("abABab", r"s/b/c/g", "abABab".replace('b', bold('c'))), # g (global) flag
("ABabAB", r"s/b/c/i", f"A{bold('c')}abAB"), # i (case-insensitive) flag
("ABabAB", r"s/b/c/ig", f"A{bold('c')}a{bold('c')}A{bold('c')}"), # both flags
)


@pytest.mark.parametrize('original, command, result', REPLACES_THAT_WORK)
def test_valid_replacements(bot, irc, user, channel, original, command, result):
"""Verify that basic replacement functionality works."""
irc.channel_joined(channel, [user.nick])

irc.say(user, channel, original)
irc.say(user, channel, command)

assert len(bot.backend.message_sent) == 1, (
"The bot should respond with exactly one line.")
assert bot.backend.message_sent == rawlist(
"PRIVMSG %s :%s meant to say: %s" % (channel, user.nick, result),
)


def test_multiple_users(bot, irc, user, other_user, channel):
"""Verify that correcting another user's line works."""
irc.channel_joined(channel, [user.nick, other_user.nick])

irc.say(other_user, channel, 'Some weather we got yesterday')
irc.say(user, channel, '%s: s/yester/to/' % other_user.nick)

assert len(bot.backend.message_sent) == 1, (
"The bot should respond with exactly one line.")
assert bot.backend.message_sent == rawlist(
"PRIVMSG %s :%s thinks %s meant to say: %s" % (
channel, user.nick, other_user.nick,
f"Some weather we got {bold('to')}day",
),
)


def test_replace_the_replacement(bot, irc, user, channel):
"""Verify replacing text that was already replaced."""
irc.channel_joined(channel, [user.nick])

irc.say(user, channel, 'spam')
irc.say(user, channel, 's/spam/eggs/')
irc.say(user, channel, 's/eggs/bacon/')

assert len(bot.backend.message_sent) == 2, (
"The bot should respond twice.")
assert bot.backend.message_sent == rawlist(
"PRIVMSG %s :%s meant to say: %s" % (
channel, user.nick, bold('eggs'),
),
"PRIVMSG %s :%s meant to say: %s" % (
channel, user.nick, bold('bacon'),
),
)

0 comments on commit b1fa914

Please sign in to comment.