128 lines
4.3 KiB
Python
128 lines
4.3 KiB
Python
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Sequence, Callable, Optional, Literal, TypeAlias, TypeVar
|
|
|
|
_invalid_file_char_regex = re.compile(r'[^a-zA-Z0-9_]')
|
|
_invalid_path_char_regex = re.compile(r'[^a-zA-Z0-9_/]')
|
|
_variable_regex = re.compile(r'@([A-Z_]+)@')
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
T = TypeVar('T')
|
|
_Validator : TypeAlias = Callable[[T], bool]
|
|
|
|
def script_preamble() -> None:
|
|
logging.basicConfig(level=logging.DEBUG, format='%(message)s')
|
|
_root = Path(__file__).parent.parent.parent
|
|
os.chdir(_root)
|
|
|
|
def run_script(main_fn: Callable[[],None]) -> None:
|
|
script_preamble()
|
|
try:
|
|
main_fn()
|
|
except KeyboardInterrupt:
|
|
_logger.warning('Cancelled.')
|
|
sys.exit(2)
|
|
except Exception as e:
|
|
_logger.error('There was an error running the script: %s', e)
|
|
sys.exit(1)
|
|
|
|
|
|
def exec_checked(args: Sequence[str], **kwargs) -> None:
|
|
subprocess.run(args, stdout=sys.stdout, stderr=sys.stderr, check=True, **kwargs)
|
|
|
|
def exec_get_output(args: Sequence[str], **kwargs) -> str:
|
|
return subprocess.run(args, text=True, check=True, capture_output=True).stdout
|
|
|
|
def get_project_config() -> dict:
|
|
raw = exec_get_output(('scons', '-s', '--disable_auto_update', '--dump=config', '--dump_format=json'))
|
|
return json.loads(raw)
|
|
|
|
def prompt(prefix: str = '> ') -> str:
|
|
sys.stdout.write(prefix)
|
|
sys.stdout.flush()
|
|
return sys.stdin.readline()
|
|
|
|
|
|
def prompt_and_validate(message: str, validator: _Validator[str], prefix: str = '> ') -> str:
|
|
while True:
|
|
print(message)
|
|
input = prompt(prefix)
|
|
if validator(input):
|
|
return input
|
|
|
|
def prompt_choices[T](message: str, choices: dict[str, T], prefix: str = '> ') -> T:
|
|
while True:
|
|
print(f'{message} [{", ".join(choices.keys())}]')
|
|
val = prompt(prefix).strip().lower()
|
|
for name, value in choices.items():
|
|
if name.lower() == val:
|
|
return value
|
|
|
|
def prompt_yesno(message: str, prefix: str = '> ') -> bool:
|
|
while True:
|
|
print(f'{message} [(y)es, (n)o]')
|
|
val = prompt(prefix).strip().lower()
|
|
if val in ('y', 'yes'):
|
|
return True
|
|
if val in ('n', 'no'):
|
|
return False
|
|
|
|
def prompt_path(message: str,
|
|
prefix: str = '> ',
|
|
mode: Literal['save']|Literal['load']|None = None,
|
|
allow_slash: bool = False,
|
|
allow_absolute: bool = False,
|
|
relative_to: Optional[Path] = None,
|
|
validator: Optional[_Validator[Path]] = None) -> Path:
|
|
while True:
|
|
print(message)
|
|
val = prompt(prefix).strip()
|
|
if val == '':
|
|
continue
|
|
if mode != 'load': # no verification needed when loading a file
|
|
regex = _invalid_path_char_regex if allow_slash else _invalid_file_char_regex
|
|
invalid_chars = regex.findall(val)
|
|
if len(invalid_chars) > 0:
|
|
print(f'Path contains the following invalid chars: {", ".join(invalid_chars)}')
|
|
continue
|
|
path = Path(val)
|
|
if not allow_absolute and path.is_absolute():
|
|
print('Absolute paths are not allowed.')
|
|
continue
|
|
if relative_to is not None:
|
|
path = relative_to / path
|
|
if mode == 'load':
|
|
if not path.exists():
|
|
print('File does not exist.')
|
|
continue
|
|
elif mode == 'save':
|
|
if path.exists():
|
|
if not prompt_yesno(f'File {path} already exists. Continue?'):
|
|
continue
|
|
if validator is not None and not validator(path):
|
|
continue
|
|
return path
|
|
|
|
|
|
|
|
def escape_filename(name: str) -> str:
|
|
return _invalid_file_char_regex.sub('_', name)
|
|
|
|
def replace_in_file(file: Path, **replacements) -> None:
|
|
_logger.debug('Patching file "%s"...', file)
|
|
bakfile = file.with_suffix(f'{file.suffix}.bak')
|
|
bakfile.unlink(missing_ok=True)
|
|
file.rename(bakfile)
|
|
|
|
with bakfile.open('r') as fin, file.open('w') as fout:
|
|
for line in fin:
|
|
def _repl(match: re.Match) -> str:
|
|
return replacements.get(match.group(1), '<invalid variable>')
|
|
fout.write(_variable_regex.sub(_repl, line))
|
|
bakfile.unlink() |