In my ongoing quest for an IRC client, I'm working on more stringent validation per RFC 2812. As with everything else I'm doing on this project, I'm trying to implement it all from scratch for the sake of joy/frustration and education. Thus I'm not going to be using any other libraries or tools designed to work with IRC clients/servers/messages/etc.
I've written this regex to validate a message. It's a little hairy, so I'm wondering if there's a way to reduce repetition, or at least to make it cleaner and easier to read. I didn't always add comments when I felt a section was easy to read, or if it was identical to another one I had previously commented.
I've also thought about maybe splitting it up into the different sections of prefix, command, and parameters, and then validating each separately.
Alternately, do you think that ditching regexes entirely might be a better solution? Right now I do consider this readable and maintainable, albeit with some effort, but I'd like to know if you feel the same.
import re
message_regex = re.compile(
r"""
# Validation of RFC 2812 messages
# https://www.rfc-editor.org/rfc/rfc2812#page-6
^ # Match from the start
# Optional prefix
( # leading colon required
:(?P<prefix>
# prefix group can be the server's name, which consists of
(?P<server_name>
[a-zA-Z0-9] # A leading alphanumeric character
[a-zA-Z0-9\-]* # Followed by the same with hyphens
[a-zA-Z0-9]* # Can't have the final character be a hyphen
# Then the above with a leading period
(\.[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]*)*
) | # otherwise it can be a nickname
(?P<nickname>
[a-zA-Z\x5B-\x60\x7B-\x7D] # letter or special character
[a-zA-Z0-9\x5B-\x60\x7B-\x7D\-]{,8} # up to 8 of the same, plus digits/hyphens
# followed by an optional host, which in turn has an optional user
(
# Any octet but NULL, CR, LF, space, or at-sign
(!(?P<user>[^\x00\x0A\x0D\x20\x40]{1,}))?
@(?P<host> # The host has a leading at sign
# Either a server name
(?P<hostname>
[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]*
(\.[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])*
)|
# Or a host address
(?P<hostaddr>
(?P<ip4address> (\d{1,3}\.){3}\d{1,3}) |
(?P<ip6address>
# Either a hex sequence followed by 7
# more colon-separated hex sequences
([a-fA-F0-9]{1,}(:[a-fA-F0-9]{1,}){7})|
# Or this thing followed by an ip4 address
0:0:0:0:0:(0|FFFF):(\d{1,3}\.){3}\d{1,3}
)
)
)
)?
)
)
[ ] # trailing space
)?
# Then a command
(?P<command>[a-zA-Z]{1,}|\d{3})
# Then optional parameters
(?P<parameters>
(
# space followed by non-null, crlf, space, colon up to 14 times
(
[ ][^\x00\x0A\x0D\x20\x3A]
# Then a colon or the same as above, minus the space
(:|[^\x00\x0A\x0D\x20\x3A])*
){,14}
# then the last element, which is the above but clumped into one
([ ]:(:|[ ]|[^\x00\x0A\x0D\x20\x3A])*)?
)|
(
# identical to above, except exactly 14 of the first, then
# the first of the last-segment's colons is optional
([ ][^\x00\x0A\x0D\x20\x3A](:|[^\x00\x0A\x0D\x20\x3A])*){14}
([ ]:?(:|[ ]|[^\x00\x0A\x0D\x20\x3A])*)?
)
)
# Then a newline
\r\n
$ # to the end
"""
, re.VERBOSE)
Here are some sample messages:
:Macha!~macha@unaffiliated/macha PRIVMSG #botwar :Test response
USER username 0 * :Real name
PING :message
:source JOIN :#channel
:source PART #channel :reason
:source QUIT :reason