#!/usr/bin/env python3 import re import sys import argparse import os import stat from dataclasses import dataclass from typing import List, Union def find_line_column(code, pos): last_newline = code.rfind('\n', 0, pos) line = code.count('\n', 0, pos) if last_newline == -1: column = pos else: column = pos - last_newline - 1 return (line, column) def stringify_code_pos(code, pos): line, column = find_line_column(code, pos) return f"{line+1}:{column+1}" TOKEN_SPECIFICATION = [ ('IMPORT', r'import'), ('OPTIONS', r'options'), ('EVAL', r'eval'), ('OPTION', r'option'), ('DMENU', r'dmenu'), ('LPAREN', r'\('), ('RPAREN', r'\)'), ('STRING', r'"(?:\\.|[^"\\])*"|\'[^\']*\'|`[^`]*`'), ('SKIP', r'[\n \t]+'), ('MISMATCH', r'.'), ] TOKEN_REGEX = '|'.join(f'(?P<{name}>{pattern})' for name, pattern in TOKEN_SPECIFICATION) @dataclass class Token: type: str value: str pos: int def __repr__(self): return f'Token({self.type}, {self.value}, pos={self.pos})' def tokenize(code, filename): tokens = [] for mo in re.finditer(TOKEN_REGEX, code): kind = mo.lastgroup value = mo.group() pos = mo.start() if kind == 'SKIP': continue elif kind == 'MISMATCH': raise RuntimeError(f'Unexpected token {value!r} at position {stringify_code_pos(code, pos)}. File {filename}') else: tokens.append(Token(kind, value, pos)) return tokens def restore_string(string_tkn_val: str): if string_tkn_val[0] == '"': return string_tkn_val[1:-1].replace("\\\"", "\"").replace("\\\\", "\\") else: return string_tkn_val[1:-1] class ActionDmenu: ... @dataclass class OptionInDmenu: label: str action: Union[ActionDmenu, str] def __repr__(self): if isinstance(self.action, ActionDmenu): return f"option {self.label} {self.action}" else: return f"option {self.label} eval {self.action}" @dataclass class ActionDmenu: prompt: str options: List[OptionInDmenu] def __repr__(self): return f"dmenu {self.prompt} ( {' ; '.join(map(str, self.options))} )" def resolve_path_relative_to_file(path_a, path_b): if os.path.isabs(path_b): return path_b a_dir = os.path.dirname(os.path.abspath(path_a)) path_c = os.path.normpath(os.path.join(a_dir, path_b)) return path_c class Parser: def __init__(self, tokens, filename, code): self.tokens = tokens self.pos = 0 self.filename = filename self.code = code def current(self): if self.pos < len(self.tokens): return self.tokens[self.pos] return None def eat(self, token_type) -> Token: token = self.current() if token is None: raise RuntimeError("Unexpected end of input") if token.type == token_type: self.pos += 1 return token raise RuntimeError(f"Expected token {token_type} at position {stringify_code_pos(self.code, self.pos)} but got {token.type}. File {self.filename}") # Grammar production: