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
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
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