ex-cli 1.9.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
from itertools import zip_longest
from subprocess import Popen, PIPE, STDOUT

class FileSystem:

    def __init__(self):
        self.home = os.environ['HOME']
        self.dates = dict()

    def remove_dir(self, path):
        path = self.append_path(path)
        try:
            shutil.rmtree(path)
            print(f'Removed dir: {path}')
        except FileNotFoundError:
            pass

    def create_dir(self, mode, size, date, path, link=None):
        path = self.append_path(path)
        if link:
            link = self.append_path(link)
            try:
                self.create_dir(mode, size, date, link)
                os.symlink(link, path)
            except FileExistsError:
                pass
        else:
            if date:
                self.dates[path] = date
            try:
                os.mkdir(path)
                print(f'Created dir: {path}')
            except FileExistsError:
                pass
            os.chmod(path, mode)

    def create_file(self, mode, size, date, path, link=None):
        path = self.append_path(path)
        if link:
            link = self.append_path(link)
            try:
                self.create_file(mode, size, date, link)
                os.symlink(link, path)
            except FileExistsError:
                pass
        else:
            if date:
                self.dates[path] = date
            with open(path, 'wb') as file:
                data = bytes(size)
                file.write(data)
            print(f'Created file: {path}')
            os.chmod(path, mode)

    def append_path(self, path):
        return os.path.join(self.home, 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):
        self.program = os.path.join(os.path.dirname(__file__), 'target', 'debug', 'ex')

    def modify_paths(self, paths):
        if paths:
            for path in paths:
                temp = f'{path}.___'
                with open(path, 'r') as reader:
                    with open(temp, 'w') as writer:
                        dirty = self.modify_stream(reader, writer)
                if dirty:
                    print(path)
                    os.rename(temp, path)
                else:
                    os.remove(temp)
        else:
            self.modify_stream(sys.stdin, sys.stdout)

    def modify_stream(self, reader, writer):
        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, writer, directory, args, windows):
        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):
        for line in reader:
            if re.search(r'^```$', line):
                break
            yield line

    def read_process(self, directory, args, windows):
        if self.modify_args(args):
            windows = True
        command = [self.program, '--now', '2024-01-01T00:00:00Z']
        command.extend(args)
        cwd = os.path.expanduser(f'~/{directory}')
        user = os.environ['USER']
        regex = re.compile(rf'\b{user}\b')
        trans = str.maketrans('\u251c\u2514\u2500\u2502', '+\\-|')
        process = Popen(command, stdout=PIPE, stderr=STDOUT, cwd=cwd, text=True)
        for line in process.stdout:
            line = regex.sub('username', line)
            line = line.translate(trans)
            if windows:
                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)
            yield line

    @staticmethod
    def modify_args(args):
        windows = False
        try:
            index = args.index('|')
            del args[index:]
        except ValueError:
            args.append('--terminal')
        for index in range(len(args)):
            if match := re.search(r'^-(\w*)w(\w*)$', args[index]):
                prefix, suffix = match.groups()
                args[index] = f'-{prefix}{suffix}'
                windows = True
        return windows

def parse_args():
    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()
    if settings.create:
        system = FileSystem()
        system.remove_dir('example')
        system.remove_dir('numbers')
        system.remove_dir('ordered')
        system.create_dir(0o755, 0, None, 'example')
        system.create_dir(0o755, 0, None, 'numbers')
        system.create_dir(0o755, 0, None, 'ordered')
        system.create_file(0o744, 10, '2023-11-01', 'example/find.sh')
        system.create_dir(0o755, 0, '2023-12-31', 'example/.hidden')
        system.create_file(0o744, 15, '2023-12-31', 'example/.hidden/password.dat')
        system.create_file(0o744, 15, '2023-12-31', 'example/.hidden/secret.dat')
        system.create_dir(0o755, 0, '2023-12-31', 'example/files')
        system.create_dir(0o755, 0, '2023-12-31', 'example/files/colours')
        system.create_file(0o744, 20, '2023-10-01', 'example/files/colours/alpha.sh')
        system.create_file(0o644, 30, '2023-09-01', 'example/files/colours/blue.txt')
        system.create_file(0o644, 40, '2023-08-01', 'example/files/colours/green.txt')
        system.create_file(0o644, 50, '2023-07-01', 'example/files/colours/red.txt')
        system.create_dir(0o755, 0, '2023-12-31', 'example/files/numbers')
        system.create_file(0o744, 60, '2023-06-01', 'example/files/numbers/count.sh', 'numbers/count.sh')
        system.create_file(0o644, 999999, '2023-05-01', 'example/files/numbers/googolplex.gz', 'numbers/googolplex.gz')
        system.create_dir(0o644, 0, '2023-04-01', 'example/files/numbers/ordinals', 'numbers/ordinals')
        system.create_dir(0o755, 0, '2023-12-31', 'example/files/numbers/one two')
        system.create_file(0o644, 70, '2023-03-01', 'example/files/numbers/one two/"three" \'four\'.txt')
        system.create_file(0o664, 0, '2023-01-01', 'ordered/file8.txt')
        system.create_file(0o664, 0, '2023-01-01', 'ordered/file9.txt')
        system.create_file(0o664, 0, '2023-01-01', 'ordered/file10.txt')
        system.create_file(0o664, 0, '2023-01-01', 'ordered/file11.txt')
        system.create_file(0o664, 0, '2023-01-01', 'ordered/file98.txt')
        system.create_file(0o664, 0, '2023-01-01', 'ordered/file99.txt')
        system.create_file(0o664, 0, '2023-01-01', 'ordered/file100.txt')
        system.create_file(0o664, 0, '2023-01-01', 'ordered/file101.txt')
        system.set_times()
    elif settings.remove:
        system = FileSystem()
        system.remove_dir('example')
        system.remove_dir('numbers')
        system.remove_dir('ordered')
    else:
        modifier = FileModifier()
        modifier.modify_paths(settings.paths)

try:
    run_main()
except OSError as error:
    print(error)
except KeyboardInterrupt:
    pass