ex-cli 1.11.0

Command line tool to find, filter, sort and list files.
Documentation
#!/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