codekingpro/portable-devtools
115k
1import codecs2import re3from typing import (4 IO,5 Iterator,6 Match,7 NamedTuple,8 Optional,9 Pattern,10 Sequence,11)12 13 14def make_regex(string: str, extra_flags: int = 0) -> Pattern[str]:15 return re.compile(string, re.UNICODE | extra_flags)16 17 18_newline = make_regex(r"(\r\n|\n|\r)")19_multiline_whitespace = make_regex(r"\s*", extra_flags=re.MULTILINE)20_whitespace = make_regex(r"[^\S\r\n]*")21_export = make_regex(r"(?:export[^\S\r\n]+)?")22_single_quoted_key = make_regex(r"'([^']+)'")23_unquoted_key = make_regex(r"([^=\#\s]+)")24_equal_sign = make_regex(r"(=[^\S\r\n]*)")25_single_quoted_value = make_regex(r"'((?:\\'|[^'])*)'")26_double_quoted_value = make_regex(r'"((?:\\"|[^"])*)"')27_unquoted_value = make_regex(r"([^\r\n]*)")28_comment = make_regex(r"(?:[^\S\r\n]*#[^\r\n]*)?")29_end_of_line = make_regex(r"[^\S\r\n]*(?:\r\n|\n|\r|$)")30_rest_of_line = make_regex(r"[^\r\n]*(?:\r|\n|\r\n)?")31_double_quote_escapes = make_regex(r"\\[\\'\"abfnrtv]")32_single_quote_escapes = make_regex(r"\\[\\']")33 34 35class Original(NamedTuple):36 string: str37 line: int38 39 40class Binding(NamedTuple):41 key: Optional[str]42 value: Optional[str]43 original: Original44 error: bool45 46 47class Position:48 def __init__(self, chars: int, line: int) -> None:49 self.chars = chars50 self.line = line51 52 @classmethod53 def start(cls) -> "Position":54 return cls(chars=0, line=1)55 56 def set(self, other: "Position") -> None:57 self.chars = other.chars58 self.line = other.line59 60 def advance(self, string: str) -> None:61 self.chars += len(string)62 self.line += len(re.findall(_newline, string))63 64 65class Error(Exception):66 pass67 68 69class Reader:70 def __init__(self, stream: IO[str]) -> None:71 self.string = stream.read()72 self.position = Position.start()73 self.mark = Position.start()74 75 def has_next(self) -> bool:76 return self.position.chars < len(self.string)77 78 def set_mark(self) -> None:79 self.mark.set(self.position)80 81 def get_marked(self) -> Original:82 return Original(83 string=self.string[self.mark.chars : self.position.chars],84 line=self.mark.line,85 )86 87 def peek(self, count: int) -> str:88 return self.string[self.position.chars : self.position.chars + count]89 90 def read(self, count: int) -> str:91 result = self.string[self.position.chars : self.position.chars + count]92 if len(result) < count:93 raise Error("read: End of string")94 self.position.advance(result)95 return result96 97 def read_regex(self, regex: Pattern[str]) -> Sequence[str]:98 match = regex.match(self.string, self.position.chars)99 if match is None:100 raise Error("read_regex: Pattern not found")101 self.position.advance(self.string[match.start() : match.end()])102 return match.groups()103 104 105def decode_escapes(regex: Pattern[str], string: str) -> str:106 def decode_match(match: Match[str]) -> str:107 return codecs.decode(match.group(0), "unicode-escape") # type: ignore108 109 return regex.sub(decode_match, string)110 111 112def parse_key(reader: Reader) -> Optional[str]:113 char = reader.peek(1)114 if char == "#":115 return None116 elif char == "'":117 (key,) = reader.read_regex(_single_quoted_key)118 else:119 (key,) = reader.read_regex(_unquoted_key)120 return key121 122 123def parse_unquoted_value(reader: Reader) -> str:124 (part,) = reader.read_regex(_unquoted_value)125 return re.sub(r"\s+#.*", "", part).rstrip()126 127 128def parse_value(reader: Reader) -> str:129 char = reader.peek(1)130 if char == "'":131 (value,) = reader.read_regex(_single_quoted_value)132 return decode_escapes(_single_quote_escapes, value)133 elif char == '"':134 (value,) = reader.read_regex(_double_quoted_value)135 return decode_escapes(_double_quote_escapes, value)136 elif char in ("", "\n", "\r"):137 return ""138 else:139 return parse_unquoted_value(reader)140 141 142def parse_binding(reader: Reader) -> Binding:143 reader.set_mark()144 try:145 reader.read_regex(_multiline_whitespace)146 if not reader.has_next():147 return Binding(148 key=None,149 value=None,150 original=reader.get_marked(),151 error=False,152 )153 reader.read_regex(_export)154 key = parse_key(reader)155 reader.read_regex(_whitespace)156 if reader.peek(1) == "=":157 reader.read_regex(_equal_sign)158 value: Optional[str] = parse_value(reader)159 else:160 value = None161 reader.read_regex(_comment)162 reader.read_regex(_end_of_line)163 return Binding(164 key=key,165 value=value,166 original=reader.get_marked(),167 error=False,168 )169 except Error:170 reader.read_regex(_rest_of_line)171 return Binding(172 key=None,173 value=None,174 original=reader.get_marked(),175 error=True,176 )177 178 179def parse_stream(stream: IO[str]) -> Iterator[Binding]:180 reader = Reader(stream)181 while reader.has_next():182 yield parse_binding(reader)183 