Created
April 17, 2023 20:37
-
-
Save arseniiv/488e741409773c701a6d7b1aefe771c9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
sfzdefiner.py | |
a tool to dump all contents of files linked to | |
specified SFZ file(s) into self-contained results; | |
replaces variables with values too | |
basic usage: (python3 | py) sfzdefiner.py (source-file-name)+ | |
usage help: (python3 | py) sfzdefiner.py --help | |
best with Python 3.11; might work on 3.10 | |
for earlier versions, you’ll need to desugar `match` statements | |
""" | |
from __future__ import annotations | |
from enum import IntEnum | |
from typing import Final, Iterable, Literal, Protocol, Sequence, TypeAlias, assert_never, cast | |
import re | |
import sys | |
from os import fspath | |
from pathlib import Path | |
import argparse as ap | |
class Args(Protocol): | |
filename: Sequence[str] | |
postfix: str | |
trace: bool | |
class ExitCode(IntEnum): | |
# NORMAL = 0 | |
NO_FILES = 1 | |
RECURSION = 2 | |
Token: TypeAlias = (str | tuple[Literal['define'], str, str] | | |
tuple[Literal['include'], str] | | |
tuple[Literal['use'], str, int, int]) | |
def pos_string(filename: str, lineno: int, column: int) -> str: | |
return f'[{filename} {lineno}:{column}]' | |
def tokenizer_re() -> re.Pattern[str]: | |
pat = r''' | |
(?P<newline> \n )| | |
(?P<define> \# define [ \t] \$ (?P<def_var> [A-Z] [A-Z0-9_]* ) | |
[ \t] (?P<def_val> [-+_A-Za-z0-9#.]+ ) | |
[ \t]* (?= \n | \Z ) )| | |
(?P<define_bad> \# define [ \t] (?: \S+ [ \t] )? \S* )| | |
(?P<include> \# include [ \t] " (?P<inc_path> .*? ) " )| | |
(?P<include_bad> \# include )| | |
(?P<use> \$ (?P<use_var> [A-Z] [A-Z0-9_]* ) )| | |
(?P<use_bad> \$ )| | |
(?P<comment> // .*? (?= \n | \Z ) )| | |
(?P<text> (?: [^\n#$/] | \# (?! define | include ) | / (?! / ) )+ )| | |
(?P<unknown> . ) | |
''' | |
return re.compile(pat, re.VERBOSE) | |
def tokenize(source: str, name: str) -> Iterable[Token]: | |
lineno = 1 | |
line_start = 0 | |
for mo in re.finditer(tokenizer_re(), source): | |
kind = mo.lastgroup | |
value = mo.group() | |
column = mo.start() - line_start + 1 | |
pos = pos_string(name, lineno, column) | |
match kind: | |
case 'newline': | |
line_start = mo.end() | |
lineno += 1 | |
yield value | |
case 'define': | |
yield kind, mo.group('def_var'), mo.group('def_val') | |
case 'define_bad': | |
print(f'{pos} bad #define (left as is): {value}', file=sys.stderr) | |
yield value | |
case 'include': | |
yield kind, mo.group('inc_path') | |
case 'include_bad': | |
print(f'{pos} bad #include (left as is)', file=sys.stderr) | |
yield value | |
case 'use': | |
yield kind, mo.group('use_var'), lineno, column | |
case 'use_bad': | |
print(f'{pos} bad $-variable (left as is)', file=sys.stderr) | |
yield value | |
case 'comment' | 'text': | |
yield value | |
case 'unknown': | |
print(f'{pos} unknown character (left as is): {value}', file=sys.stderr) | |
yield value | |
case _: | |
raise ValueError(f'unknown token kind: {kind}') | |
def process(in_p: Path, out_p: Path, *, do_trace: bool) -> None: | |
root_path = in_p.parent | |
opened_files: list[Path] = [] | |
defined_vars: dict[str, str] = dict() | |
chunks: list[str] = [] | |
def include_file(path: Path) -> None: | |
old_path = fspath(path) | |
if not path.is_absolute(): | |
path = root_path / path | |
if not path.exists() or not path.is_file(): | |
if old_path != fspath(path): | |
add = '' | |
else: | |
add = f' (‘{fspath(path)}’)' | |
print(f'Can’t process the file ‘{old_path}’{add}.', file=sys.stderr) | |
return | |
for i, opened_p in enumerate(opened_files): | |
if path.samefile(opened_p): | |
print('Recursive #include in files:') | |
for opened_p_ in opened_files[i:]: | |
print(f'\t{fspath(opened_p_)}') | |
print(f'\t{fspath(path)} <- the same as the first') | |
sys.exit(ExitCode.RECURSION) | |
opened_files.append(path) | |
chunks.append('\n') | |
trace_here: Final = do_trace and len(opened_files) > 1 | |
if trace_here: | |
trace_str = ' > '.join(map(lambda p: p.name, opened_files[1:])) | |
chunks.append(f'\n//INC-START[ > {trace_str} ]\n\n') | |
raw_contents = path.read_text(encoding='utf8') | |
for token in tokenize(raw_contents, path.name): | |
match token: | |
case str(x): | |
chunks.append(x) | |
case 'define', str(def_var), str(def_val): | |
defined_vars[def_var] = def_val | |
case 'include', str(inc_path): | |
include_file(Path(inc_path)) | |
case 'use', str(use_var), int(lineno), int(column): | |
pos = pos_string(path.name, lineno, column) | |
use_val = defined_vars.get(use_var) | |
if use_val is None: | |
print(f'{pos} variable is undefined: ${use_var} (left as is).', file=sys.stderr) | |
use_val = '$' + use_var | |
chunks.append(use_val) | |
case _: | |
assert_never(token) | |
if trace_here: | |
chunks.append(f'\n//INC-END[ > {trace_str} ]\n') | |
opened_files.pop() | |
chunks.append('\n') | |
include_file(in_p) | |
out_p.write_text(''.join(chunks), encoding='utf8') | |
FileQueue: TypeAlias = list[tuple[Path, Path]] | |
def main() -> None: | |
parser = ap.ArgumentParser( | |
prog='sfzdefiner', | |
description='SFZ de-finer by 05deg', | |
epilog='🐱💻') | |
parser.add_argument('filename', nargs='*', | |
help='an input filename') | |
parser.add_argument('-p', '--postfix', default='-expanded', | |
help='postfix of output filenames (def: ‘-expanded’)') | |
parser.add_argument('--trace', action='store_true', | |
help='trace #include enters and exits') | |
args = cast('Args', parser.parse_args()) | |
if not args.filename: | |
print('Nothing to process! 🤔') | |
return | |
file_queue: list[tuple[Path, Path]] = [] | |
for in_p in args.filename: | |
in_p = Path(in_p) | |
if not in_p.exists(): | |
print(f'The path ‘{fspath(in_p)}’ doesn’t exist; it will be skipped.') | |
if not in_p.is_file(): | |
print(f'The path ‘{fspath(in_p)}’ is not a file; it will be skipped.') | |
out_p = in_p.with_stem(in_p.stem + args.postfix) | |
file_queue.append((in_p, out_p)) | |
print(f'{fspath(in_p)} --> {fspath(out_p)}') | |
if out_p.exists(): | |
add = '' if out_p.is_file() else ' and is not a file' | |
print(f'!! Note that the output already exists{add}.') | |
if not file_queue: | |
print('All specified input filenames were invalid!', file=sys.stderr) | |
sys.exit(ExitCode.NO_FILES) | |
for paths in file_queue: | |
process(*paths, do_trace=args.trace) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment