#!/usr/bin/env python3 import argparse import datetime import os import re import shutil import sys import tempfile from argparse import Namespace from itertools import zip_longest from pathlib import Path from subprocess import Popen, PIPE, STDOUT from tempfile import NamedTemporaryFile from typing import Any, Optional, TextIO, Tuple, TypeAlias, Union # Use "type" statement in Python 3.12. AnyIO: TypeAlias = Union[TextIO, Any] AnyPath: TypeAlias = Union[Path, str] class FileSystem: def __init__(self): self.user = os.environ.get('USER') self.user = os.environ.get('SUDO_USER', self.user) self.uid = int(os.environ.get('SUDO_UID', os.getuid())) self.gid = int(os.environ.get('SUDO_GID', os.getgid())) self.sudoed = os.getuid() == 0 self.home = Path.home() self.temp = Path(tempfile.gettempdir()) self.dates = dict() def remove_dir(self, path: str): path = self.append_path(path) try: shutil.rmtree(path) print(f'Removed {path}') except FileNotFoundError: pass def create_dir(self, mode: int, root: bool, size: int, date: str, path: AnyPath, link: Optional[str]): path = self.append_path(path) if link: link = self.append_path(link) try: self.create_dir(mode, root, size, date, link, None) os.symlink(link, path) except FileExistsError: pass else: self.dates[path] = date try: os.mkdir(path) print(f'Created {path}') except FileExistsError: pass os.chmod(path, mode) if root: os.chown(path, 0, 0) else: os.chown(path, self.uid, self.gid) def create_file(self, mode: int, root: bool, size: int, date: str, path: AnyPath, link: Optional[str]): path = self.append_path(path) if link: link = self.append_path(link) try: self.create_file(mode, root, size, date, link, None) os.symlink(link, path) except FileExistsError: pass else: self.dates[path] = date with open(path, 'wb') as file: data = bytes(size) file.write(data) print(f'Created {path}') os.chmod(path, mode) if root: os.chown(path, 0, 0) else: os.chown(path, self.uid, self.gid) def append_path(self, path: str) -> Path: return self.temp / path def set_times(self): for path, date in reversed(self.dates.items()): date = datetime.date.fromisoformat(date) time = datetime.datetime.combine(date, datetime.datetime.min.time()) time = time.timestamp() os.utime(path, (time, time)) class FileModifier: def __init__(self, system: FileSystem): self.program = Path(__file__).parent / 'target' / 'debug' / 'ex' self.system = system # noinspection PyProtectedMember def modify_paths(self, paths: list[Path]): if paths: for path in paths: dirty = False with open(path, 'r') as reader: with NamedTemporaryFile('w', dir=path.parent) as writer: if self.modify_stream(reader, writer): dirty = True writer._closer.delete = False if dirty: print(path) os.rename(writer.name, path) else: self.modify_stream(sys.stdin, sys.stdout) def modify_stream(self, reader: TextIO, writer: AnyIO) -> bool: dirty = False quoted = False for line in reader: print(line, end='', file=writer) if quoted: if match := re.search(r'^(?:~/(\w+) \$|C:\\Users\\username\\(\w+)>) ex(?:\.exe)? ?(.*)$', line): linux, windows, args = match.groups() args = args.split() if 'xargs' not in args: if linux: dirty |= self.modify_chunk(reader, writer, linux, args, False) if windows: dirty |= self.modify_chunk(reader, writer, windows, args, True) quoted = False else: if re.search(r'^```$', line): quoted = True return dirty def modify_chunk(self, reader: TextIO, writer: AnyIO, directory: str, args: list[str], windows: bool) -> bool: dirty = False olds = self.read_existing(reader) news = self.read_process(directory, args, windows) for old, new in zip_longest(olds, news): if new is not None: print(new, end='', file=writer) dirty |= (new != old) elif old is not None: dirty = True print('```', file=writer) return dirty @staticmethod def read_existing(reader: TextIO): for line in reader: if re.search(r'^```$', line): break yield line def read_process(self, directory: str, args: list[str], windows: bool): args, windows2, version, owner = self.modify_args(args) command = [self.program, *args] local = str(self.system.temp / directory) replace = str(self.system.home.parent / 'username') regex = re.compile(rf'{self.system.temp}\b') process = Popen(command, stdout=PIPE, stderr=STDOUT, cwd=local, text=True) for line in process.stdout: line = regex.sub(replace, line) if windows or windows2: line = re.sub(r'^([dl-])([r-][w-][x-]).{6}', r'\1\2\2\2', line) line = re.sub(r'/', r'\\', line) line = re.sub(r'\\home\b', r'C:\\Users', line) if version: if match := re.search(r'^(\S+\s+\d+\s+\S+\s+\S+\s+\S+\s+)((\.\w+)\s+\S+)$', line): prefix, suffix, ext = match.groups() if ext == '.exe': insert = '2.1.0.999 ' elif ext == '.dll': insert = '2.1.0.1001 ' else: insert = ' ' line = f'{prefix}{insert}{suffix}\n' if owner: if match := re.search(rf'^(\S+\s+){self.system.user}(\s+){self.system.user}(.+)$', line): prefix, padding, suffix = match.groups() line = f'{prefix}username{padding}username{suffix}\n' yield line @staticmethod def modify_args(args: list[str]) -> Tuple[list[str], bool, bool, bool]: args2 = list() windows = False version = False owner = False piped = False for arg in args: if arg == '|': piped = True break if match := re.search(r'^-(\w*)w(\w*)$', arg): prefix, suffix = match.groups() arg = f'-{prefix}{suffix}' windows = True if match := re.search(r'^-(\w*)v(\w*)$', arg): prefix, suffix = match.groups() arg = f'-{prefix}{suffix}' version = True if arg == '--owner': owner = True if arg not in ('-', ''): args2.append(arg) if not piped: args2.append('--terminal') args2.extend(['--now', '2024-01-01T00:00:00Z']) return args2, windows, version, owner def parse_args() -> Namespace: parser = argparse.ArgumentParser() parser.add_argument('-c', '--create', action='store_true', help='create example directories') parser.add_argument('-r', '--remove', action='store_true', help='remove example directories') parser.add_argument('paths', nargs='*', default=[], help='file to modify') return parser.parse_args() def run_main(): settings = parse_args() system = FileSystem() if settings.create or settings.remove: if not system.sudoed: print('Must be run as root to create or remove files', file=sys.stderr) exit(1) system.remove_dir('bin') system.remove_dir('example') system.remove_dir('numbers') system.remove_dir('ordered') if settings.create: system.create_dir(0o755, False, 0, '2023-12-01', 'bin', None) system.create_dir(0o755, False, 0, '2023-12-01', 'example', None) system.create_dir(0o755, False, 0, '2023-12-01', 'numbers', None) system.create_dir(0o755, False, 0, '2023-12-01', 'ordered', None) system.create_file(0o777, False, 123000, '2023-12-01', 'bin/binary.exe', None) system.create_file(0o666, False, 45000, '2023-12-01', 'bin/library.dll', None) system.create_file(0o666, False, 678, '2023-12-01', 'bin/README.txt', None) system.create_file(0o744, True, 10, '2023-11-01', 'example/find.sh', None) system.create_dir(0o755, False, 0, '2023-12-31', 'example/.hidden', None) system.create_file(0o744, False, 15, '2023-12-31', 'example/.hidden/password.dat', None) system.create_file(0o744, False, 15, '2023-12-31', 'example/.hidden/secret.dat', None) system.create_dir(0o755, False, 0, '2023-12-31', 'example/files', None) system.create_dir(0o755, False, 0, '2023-12-31', 'example/files/colours', None) system.create_file(0o744, False, 20, '2023-10-01', 'example/files/colours/alpha.sh', None) system.create_file(0o644, False, 30, '2023-09-01', 'example/files/colours/blue.txt', None) system.create_file(0o644, False, 40, '2023-08-01', 'example/files/colours/green.txt', None) system.create_file(0o644, False, 50, '2023-07-01', 'example/files/colours/red.txt', None) system.create_dir(0o755, False, 0, '2023-12-31', 'example/files/numbers', None) system.create_file(0o744, False, 60, '2023-06-01', 'example/files/numbers/count.sh', 'numbers/count.sh') system.create_file(0o644, False, 999999, '2023-05-01', 'example/files/numbers/googolplex.gz', 'numbers/googolplex.gz') system.create_dir(0o644, False, 0, '2023-04-01', 'example/files/numbers/ordinals', 'numbers/ordinals') system.create_dir(0o755, False, 0, '2023-12-31', 'example/files/numbers/one two', None) system.create_file(0o644, False, 70, '2023-03-01', 'example/files/numbers/one two/"three" \'four\'.txt', None) system.create_file(0o664, False, 0, '2023-01-01', 'ordered/file8.txt', None) system.create_file(0o664, False, 0, '2023-01-01', 'ordered/file9.txt', None) system.create_file(0o664, False, 0, '2023-01-01', 'ordered/file10.txt', None) system.create_file(0o664, False, 0, '2023-01-01', 'ordered/file11.txt', None) system.create_file(0o664, False, 0, '2023-01-01', 'ordered/file98.txt', None) system.create_file(0o664, False, 0, '2023-01-01', 'ordered/file99.txt', None) system.create_file(0o664, False, 0, '2023-01-01', 'ordered/file100.txt', None) system.create_file(0o664, False, 0, '2023-01-01', 'ordered/file101.txt', None) system.set_times() else: if system.sudoed: print('Must be run as user to modify readme file', file=sys.stderr) exit(1) paths = [Path(p) for p in settings.paths] modifier = FileModifier(system) modifier.modify_paths(paths) try: run_main() except OSError as error: print(error) except KeyboardInterrupt: pass