|
#!/usr/bin/python |
|
from io import TextIOWrapper |
|
import re |
|
import sys |
|
|
|
build = open("compiled.pgsql", "w") |
|
setvals:dict[str, str|int|bool] = {} |
|
def parse_arguments(): |
|
matcher = re.compile(r'^([a-zA-Z0-9]+)(=([a-zA-Z0-9]+))?$') |
|
for arg in sys.argv[1:]: |
|
key, _, val = matcher.match(arg).groups() |
|
if val is None: |
|
setvals[key] = None |
|
elif re.match(r'[0-9]+', val): |
|
setvals[key] = int(val) |
|
elif val.lower().strip() in ('true', 'false'): |
|
setvals[key]=val=='true' |
|
elif re.match(r'^[a-zA-Z0-9]+$', val): |
|
setvals[key] = val |
|
else: |
|
raise ValueError(f"Invalid argument: {arg} (how did you do that?)") |
|
|
|
def handle_inline(line:str): |
|
if not line.startswith('--@inline'): |
|
raise ValueError(f'Inline handler called with non-inline marker {line}') |
|
fpath = line[9:].strip() |
|
if (fpath is None) or (fpath == ''): |
|
raise ValueError(f'Inline handler called with empty path {line}') |
|
with open(fpath, 'r') as f: |
|
yield f'\n--{fpath}\n' |
|
for line in f: |
|
if line.startswith('--@'): |
|
for cl in handle_component(line, f): |
|
yield cl |
|
else: |
|
yield line |
|
yield f'--End {fpath}\n' |
|
|
|
def build_conditional(condition:str): |
|
matcher = re.compile(r'([a-zA-Z0-9]+) ?([<>]=?|[=!]=)? ?([a-zA-Z0-9]+)?') |
|
groups = matcher.match(condition).groups() |
|
if groups is None: |
|
raise ValueError(f'Invalid conditional: {condition}') |
|
key, op, val = groups |
|
if op is None and val is not None: |
|
raise ValueError(f'Conditional {condition} does not have a valid operator') |
|
if (key not in setvals) and op is None:# Op being none implies val is none (^ see check ^) |
|
return False |
|
def _compareHandler(v1, op, v2): |
|
if type(v1) != type(v2): |
|
raise ValueError(f'Cannot compare {v1}({type(v1)}) and {v2}({type(v2)})') |
|
match op: |
|
case '==': |
|
if type(v1) in (int, str): |
|
return v1 == v2 |
|
if type(v1) == bool and v2 != False: |
|
print(f'[Warn]: Unnecessary operator {key}{op}{v2}: Comparing bool to true can be simplified to --@if {key}') |
|
return v1 == v2 |
|
case '!=': |
|
if type(v1) in (int, str): |
|
return v1 != v2 |
|
if type(v1) == bool and v2 == False: |
|
print(f'[Warn]: Unnecessary operator {key}{op}{v2}: Comparing bool to not false can be simplified to --@if {key}') |
|
return v1 != v2 |
|
case '<': |
|
if type(v1) == bool: |
|
raise ValueError(f'Bools cannot be numerically compared') |
|
return v1 < v2 |
|
case '>': |
|
if type(v1) == bool: |
|
raise ValueError(f'Bools cannot be numerically compared') |
|
return v1 > v2 |
|
case '<=': |
|
if type(v1) == bool: |
|
raise ValueError(f'Bools cannot be numerically compared') |
|
return v1 <= v2 |
|
case '>=': |
|
if type(v1) == bool: |
|
raise ValueError(f'Bools cannot be numerically compared') |
|
return v1 >= v2 |
|
case _: |
|
raise ValueError(f'Invalid operator: {op}') |
|
if val is not None: |
|
if re.match(r'^[0-9]+$', val): |
|
return _compareHandler(setvals[key], op, int(val)) |
|
elif val.lower().strip() in ('true', 'false'): |
|
return _compareHandler(setvals[key], op, val.lower().strip() == 'true') |
|
elif re.match(r'^[a-zA-Z0-9]+$', val): |
|
return _compareHandler(setvals[key], op, val) |
|
else: |
|
raise ValueError(f'Invalid value in conditional: {val}') |
|
else: #unary conditional |
|
return key in setvals |
|
|
|
def handle_conditional(component:str, handle:TextIOWrapper): |
|
if not component.startswith('--@if'): |
|
raise ValueError(f'Conditional handler called with non-conditional marker {line}') |
|
condition:bool = build_conditional(component[5:].strip()) |
|
if condition: # conditional active |
|
yield component# return conditonal line |
|
for line in handle: |
|
if line.startswith('--@'):# component handler |
|
if line.strip() == '--@end': |
|
yield line# return end line |
|
break |
|
elif line.strip() == '--@else': |
|
for line in handle:# inactive else block |
|
if line.startswith('--@'): |
|
for cl in handle_component(line, handle): yield cl# handle nested ifs and includes |
|
elif line.strip() != '--@end': |
|
continue# discard |
|
else: |
|
return line# end inactive else block (also end of full conditional block) |
|
else: |
|
for cl in handle_component(line, handle): yield cl# handle nested ifs and includes |
|
else: |
|
yield line# normal line |
|
else:# conditional inactive |
|
for line in handle: |
|
if line.startswith('--@'):# component handler |
|
if line.strip() == '--@end': |
|
break# end of inactive conditional block |
|
elif line.strip() == '--@else': |
|
yield f'--@else [{component[5:].strip()}]\n' |
|
for line in handle:# active else block |
|
if line.startswith('--@'): |
|
for cl in handle_component(line, handle): yield cl# handle nested ifs and includes |
|
elif line != '--@end': |
|
yield line |
|
else: |
|
return line |
|
else: |
|
continue# refuse to handle inactive components in block |
|
else: |
|
continue |
|
|
|
def handle_component(line:str, handle:TextIOWrapper): |
|
if not line.startswith('--@'): |
|
raise ValueError(f'Component handler called with non-component line {line}') |
|
match line.split(' ')[0]: |
|
case '--@inline': |
|
for line in handle_inline(line): yield line |
|
case '--@if': |
|
for line in handle_conditional(line, handle): yield line |
|
|
|
def build_file(path:str): |
|
yield f"--{path}\n" |
|
with open(path, 'r') as f: |
|
for line in f: |
|
if line.startswith('--@'): |
|
for cl in handle_component(line, f): |
|
yield cl |
|
else: |
|
yield line |
|
|
|
parse_arguments() |
|
for line in build_file('Main.sql'): |
|
build.write(line) |
|
build.close() |