import os import re import sys from dataclasses import dataclass, field from typing import List def get_tagtime_command() -> List[str]: """ The original Perl implementation spawns another Perl script in a terminal to ask the user for their current tags. To replicate this behavior we find out the python executable and the location of the tagtime.py script. """ python_exe = sys.executable tagtimerc_py = os.path.abspath(__file__) tagtime_py = tagtimerc_py.replace("tagtimerc.py", "tagtime.py") return [python_exe, tagtime_py] @dataclass class TagTimeRc: log_file: str ed: str xt: str retrothresh: int gap: int urping: int seed: int linelen: int catchup: int tags_off: List[str] = field(default_factory=lambda: ["afk", "off", "RETRO"]) tags_afk: List[str] = field(default_factory=lambda: ["afk", "RETRO"]) tags_err: List[str] = field(default_factory=lambda: ["err"]) tagtimecmd: List[str] = field(default_factory=get_tagtime_command) def value_to_int(value: str) -> int: """ The tagtimerc file is itself a Perl script. That means the assignments to the configuration parameters can be Perl expressions. We could replicate that behavior via `eval`, but we don't want to do that because it allows arbitrary code execution. Instead we only support multiplication for now (`45*60`). """ try: return int(value) except ValueError: pass result = 1 for v in value.split("*"): result *= int(v) return result def parse_tagtimerc(tagtimerc_path: str) -> TagTimeRc: """ Parses the configuration attributes from a tagtimerc file into Python. All lines that start with a dollar sign are configuration lines. These lines are than split into key and value. This function only considers attributes that are part of TagTimeRc. """ s = ("^\s*" # potential leading whitespace "\$(\w+)" # key as group (variables in Perl start with $) "\s*=\s*" # equal sign including potential whitespaces around it "\"?" # potential opening quote "([^\";]+)" # value (everything that is not a semicolon or quote) "\"?" # potential closing quote ";") # semicolon to terminate key value pair re_config_line = re.compile(s) tagtimerc_path = os.path.expanduser(tagtimerc_path) with open(tagtimerc_path, 'r') as f: key_value_pairs = {m.groups()[0].lower(): m.groups()[1] for line in f.readlines() if (m := re_config_line.match(line))} # Get dictionary of the TagTimeRc attribute types. tagtimerc_types = TagTimeRc.__annotations__ # Process the key-value-pairs. Keep expected pairs and store them into # kwarsg. kwargs = {} for key, value in key_value_pairs.items(): if key in tagtimerc_types and tagtimerc_types[key] is int: kwargs[key] = value_to_int(value) elif key == "logf": # Give special treatman to logf, because it may contain other # variables (`$logf = "$path$usr.log";`) Iterate over those # variables and replace them with their value. for variable in re.findall("\$\w+", value): var = variable.replace("$", "") value = value.replace(variable, key_value_pairs[var]) kwargs["log_file"] = value elif key in tagtimerc_types: kwargs[key] = value return TagTimeRc(**kwargs)