Snippets

Doug Freed Python IRCv3.2 Line Parser

Created by Doug Freed last modified
from enum import Enum
from typing import Dict, List, Optional, Tuple

tag_replacements = (
    (r'\:', ';'),
    (r'\s', ' '),
    (r'\r', '\r'),
    (r'\n', '\n'),
    (r'\\', '\\'),
)


class ParseState(Enum):
    tags = 1
    prefix = 2
    command = 3
    params = 4
    trailing = 5


def parse(line: str) -> Tuple[str, Dict[str, str], Optional[str], Optional[str], Optional[str], Optional[str], Optional[str], List[str], Optional[str]]:
    state = ParseState.tags
    temp = line.split(' ')
    tags, prefix, command, params, rest, nick, user, host = {}, None, None, [], None, None, None, None
    for i in range(len(temp)):
        if not temp[i]:
            continue
        elif state == ParseState.tags and temp[i][0] == '@':
            temp2 = temp[i][1:].split(';')
            for tag in temp2:
                key, _, value = tag.partition('=')
                if '\\' in value:
                    for old, new in tag_replacements:
                        value = value.replace(old, new)
                tags[key] = value
            state = ParseState.prefix
        elif state in (ParseState.tags, ParseState.prefix) and temp[i][0] == ':':
            prefix = temp[i][1:]
            if '!' in prefix:
                nick, user = prefix.split('!')
                if '@' in user:
                    user, host = user.split('@')
            elif '@' in prefix:
                nick, host = prefix.split('@')
            else:
                nick = prefix
            state = ParseState.command
        elif state in (ParseState.tags, ParseState.prefix, ParseState.command):
            command = temp[i]
            state = ParseState.params
        elif state == ParseState.trailing or state == ParseState.params and temp[i][0] == ':':
            rest = ' '.join(temp[i:])
            if rest[0] == ':':
                rest = rest[1:]
            params.append(rest)
            break
        elif state == ParseState.params:
            params.append(temp[i])
            if len(params) == 14:
                state = ParseState.trailing
        else:
            raise NotImplementedError(repr(line))  # pragma: no cover
    return line, tags, prefix, nick, user, host, command, params, rest
from parse import parse


class TestClass:
    def test_blank(self):
        line, tags, prefix, nick, user, host, command, params, rest = parse("")
        assert line == ""
        assert tags == {}
        assert prefix is None
        assert nick is None
        assert user is None
        assert host is None
        assert command is None
        assert params == []
        assert rest is None

    def test_no_tags(self):
        line = ":nick!user@host command param :rest"
        line_return, tags, prefix, nick, user, host, command, params, rest = parse(line)
        assert line_return == line
        assert tags == {}
        assert prefix == "nick!user@host"
        assert nick == "nick"
        assert user == "user"
        assert host == "host"
        assert command == "command"
        assert params == ['param', 'rest']
        assert rest == "rest"

    def test_tags_no_replacements(self):
        line = "@key1;key2=value :nick!user@host command param :rest"
        line_return, tags, prefix, nick, user, host, command, params, rest = parse(line)
        assert line_return == line
        assert tags == {'key1': '', 'key2': 'value'}
        assert prefix == "nick!user@host"
        assert nick == "nick"
        assert user == "user"
        assert host == "host"
        assert command == "command"
        assert params == ['param', 'rest']
        assert rest == "rest"

    def test_tags_replacements(self):
        line = r"@key1=\:\s\r\n\\ :nick!user@host command param :rest"
        line_return, tags, prefix, nick, user, host, command, params, rest = parse(line)
        assert line_return == line
        assert tags == {'key1': '; \r\n\\'}
        assert prefix == "nick!user@host"
        assert nick == "nick"
        assert user == "user"
        assert host == "host"
        assert command == "command"
        assert params == ['param', 'rest']
        assert rest == "rest"

    def test_tags_replacements_correct(self):
        line = r"@key1=\:\s\r\\\n :nick!user@host command param :rest"
        line_return, tags, prefix, nick, user, host, command, params, rest = parse(line)
        assert line_return == line
        assert tags == {'key1': '; \r\\\n'}
        assert prefix == "nick!user@host"
        assert nick == "nick"
        assert user == "user"
        assert host == "host"
        assert command == "command"
        assert params == ['param', 'rest']
        assert rest == "rest"

    def test_no_user(self):
        line = ":nick@host command param :rest"
        line_return, tags, prefix, nick, user, host, command, params, rest = parse(line)
        assert line_return == line
        assert tags == {}
        assert prefix == "nick@host"
        assert nick == "nick"
        assert user is None
        assert host == "host"
        assert command == "command"
        assert params == ['param', 'rest']
        assert rest == "rest"

    def test_no_host(self):
        line = ":nick!user command param :rest"
        line_return, tags, prefix, nick, user, host, command, params, rest = parse(line)
        assert line_return == line
        assert tags == {}
        assert prefix == "nick!user"
        assert nick == "nick"
        assert user == "user"
        assert host is None
        assert command == "command"
        assert params == ['param', 'rest']
        assert rest == "rest"

    def test_no_prefix(self):
        line = "command param :rest"
        line_return, tags, prefix, nick, user, host, command, params, rest = parse(line)
        assert line_return == line
        assert tags == {}
        assert prefix is None
        assert nick is None
        assert user is None
        assert host is None
        assert command == "command"
        assert params == ['param', 'rest']
        assert rest == "rest"

    def test_tags_no_prefix(self):
        line = "@key1 command param :rest"
        line_return, tags, prefix, nick, user, host, command, params, rest = parse(line)
        assert line_return == line
        assert tags == {'key1': ''}
        assert prefix is None
        assert nick is None
        assert user is None
        assert host is None
        assert command == "command"
        assert params == ['param', 'rest']
        assert rest == "rest"

    def test_no_middle(self):
        line = ":nick!user@host command :rest"
        line_return, tags, prefix, nick, user, host, command, params, rest = parse(line)
        assert line_return == line
        assert tags == {}
        assert prefix == "nick!user@host"
        assert nick == "nick"
        assert user == "user"
        assert host == "host"
        assert command == "command"
        assert params == ['rest']
        assert rest == "rest"

    def test_no_trailing(self):
        line = ":nick!user@host command param"
        line_return, tags, prefix, nick, user, host, command, params, rest = parse(line)
        assert line_return == line
        assert tags == {}
        assert prefix == "nick!user@host"
        assert nick == "nick"
        assert user == "user"
        assert host == "host"
        assert command == "command"
        assert params == ['param']
        assert rest is None

    def test_nick_only(self):
        line = ":nick command param :rest"
        line_return, tags, prefix, nick, user, host, command, params, rest = parse(line)
        assert line_return == line
        assert tags == {}
        assert prefix == "nick"
        assert nick == "nick"
        assert user is None
        assert host is None
        assert command == "command"
        assert params == ['param', 'rest']
        assert rest == "rest"

    def test_spaces(self):
        line = r"@key1=\:\s\r\n\\     :nick!user@host   command   param    :rest"
        line_return, tags, prefix, nick, user, host, command, params, rest = parse(line)
        assert line_return == line
        assert tags == {'key1': '; \r\n\\'}
        assert prefix == "nick!user@host"
        assert nick == "nick"
        assert user == "user"
        assert host == "host"
        assert command == "command"
        assert params == ['param', 'rest']
        assert rest == "rest"

    def test_spaces_in_trailing(self):
        line = r"@key1=\:\s\r\n\\     :nick!user@host   command   param    :rest     foobar"
        line_return, tags, prefix, nick, user, host, command, params, rest = parse(line)
        assert line_return == line
        assert tags == {'key1': '; \r\n\\'}
        assert prefix == "nick!user@host"
        assert nick == "nick"
        assert user == "user"
        assert host == "host"
        assert command == "command"
        assert params == ['param', 'rest     foobar']
        assert rest == "rest     foobar"

    def test_colon_in_trailing(self):
        line = r"@key1=\:\s\r\n\\     :nick!user@host   command   param    :rest     :foobar"
        line_return, tags, prefix, nick, user, host, command, params, rest = parse(line)
        assert line_return == line
        assert tags == {'key1': '; \r\n\\'}
        assert prefix == "nick!user@host"
        assert nick == "nick"
        assert user == "user"
        assert host == "host"
        assert command == "command"
        assert params == ['param', 'rest     :foobar']
        assert rest == "rest     :foobar"

    def test_15_params(self):
        line = ":nick!user@host command 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15"
        line_return, tags, prefix, nick, user, host, command, params, rest = parse(line)
        assert line_return == line
        assert nick == "nick"
        assert user == "user"
        assert host == "host"
        assert command == "command"
        assert params == ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15']
        assert rest == "15"

    def test_15_params_colon_trailing(self):
        line = ":nick!user@host command 1 2 3 4 5 6 7 8 9 10 11 12 13 14 :15"
        line_return, tags, prefix, nick, user, host, command, params, rest = parse(line)
        assert line_return == line
        assert nick == "nick"
        assert user == "user"
        assert host == "host"
        assert command == "command"
        assert params == ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15']
        assert rest == "15"

    def test_16_params(self):
        line = ":nick!user@host command 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16"
        line_return, tags, prefix, nick, user, host, command, params, rest = parse(line)
        assert line_return == line
        assert nick == "nick"
        assert user == "user"
        assert host == "host"
        assert command == "command"
        assert params == ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15 16']
        assert rest == "15 16"

    def test_15_params_colon_trailing_multiword(self):
        line = ":nick!user@host command 1 2 3 4 5 6 7 8 9 10 11 12 13 14 :15 16"
        line_return, tags, prefix, nick, user, host, command, params, rest = parse(line)
        assert line_return == line
        assert nick == "nick"
        assert user == "user"
        assert host == "host"
        assert command == "command"
        assert params == ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15 16']
        assert rest == "15 16"

    def test_16th_param_colon(self):
        line = ":nick!user@host command 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 :16"
        line_return, tags, prefix, nick, user, host, command, params, rest = parse(line)
        assert line_return == line
        assert nick == "nick"
        assert user == "user"
        assert host == "host"
        assert command == "command"
        assert params == ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15 :16']
        assert rest == "15 :16"

Comments (2)

HTTPS SSH

You can clone a snippet to your computer for local editing. Learn more.