Как стать автором
Обновить

Как новичок пытался написать свой «терминал»

Уровень сложностиСредний
Время на прочтение4 мин
Количество просмотров1.7K

В этой статье я бы хотел рассказать о том, как начал писать свой "терминал" (хотя скорее это кастомный CLI). По умолчанию встроенный в винду терминал не является самым удобным инструментом. На текущий момент конечно есть некоторые эмуляторы терминала с дополнениями, но я решил сделать свое. И вот что из этого вышло.

Предупреждение от автора

Я не являюсь Senior программистом и не являюсь богом всея кода на планете. Я обычный 10-классник, пытающийся сделать что-то хоть немного интересное, а не только калькулятор. Это мой первый open-source проект, поэтому не ругайте сильно.

Начало пути

Для начала, чтобы понять, а что я хочу, я залез в интернет почитать об интерфейсе командной строки и как её можно изменять. В итоге получил информацию, что терминал - CLI, а различные её приложения (по типу Node.JS, Django и т.д.) - CLI-Apps. По определению CLI - это и есть интерфейс командной строки.

Дальше нужно было выбрать язык, на котором я хотел писать свою "оболочку". Выбор, как у великого новичка пал на такой язык как Python. Он легкий, удобный, по скорости его вполне хватает.

Начало разработки

Начал свою разработку с изучения различных встроенных библиотек питона, по типу os, sys, typing и других. Изучив их, я понял, что они отлично взаимодействуют с командной строкой и они подойдут для её видоизменения. Поэтому начал писать свой код.


Первые шаги

Для начала написания кода пришлось определить структуру приложения. Нужно было создать удобную структуру, чтобы вся логика приложения не хранилась в одном файле. Немного поразмыслив, я решил сделать такую структуру:

main.py
config.py
commands_controller.py
commands.py
sys_controller.py
modules/
│
├── example.py

В файле main.py основной класс обработчик, который перенаправляет команды пользователя в специальный контроллер команд.

class Terminode:
    def __init__(self):
        self.version = config.console_version
        self.cur_dir = os.getcwd()
        self.username = config.username
        self.input_line = f"{self.username} | {self.cur_dir} | "
        self.commands = commands_return()

    def parse_input(self, input_str: str) -> List[str]:
        return input_str.strip().split()

    def run(self):
        print(f"Terminode - {self.version}")
        print("Enter 'help' for commands list / Enter 'exit' for exit app")
        
        while True:
            try:
                user_input = input(self.input_line).strip()
                if not user_input:
                    continue
                
                parts = self.parse_input(user_input)
                command_name = parts[0]
                args = parts[1:] if len(parts) > 1 else None
                
                if command_name in self.commands:
                    self.commands[command_name](args)
                else:
                    execute_system_command(user_input)
                self.update_prompt()
                    
            except KeyboardInterrupt:
                print("\nFor quit enter 'exit'")

            except EOFError:
                print()
                self.exit_command()

            except Exception as e:
                print(f"Error: {e}")

    def update_prompt(self):
        self.input_line = f"{self.username} | {os.getcwd()} | "

Данный класс проверяет команду на существование её в списке "кастомных команд". Если её нет в списке, то терминал пытается выполнить команду, как системную - встроенную в стандартный терминал с помощью файла sys_controller.py. Также при каждом сообщении обновляется строка ввода, чтобы пользователь мог видеть текущую директорию, в которой он находится.

Все команды проверяются из файла commands.py . Каждая функция в нём начинается с декоратора @command, которая отвечает за регистрацию команды в терминале. Вот пример одной из команд:

@command(name='time')
def time_command(args: List[str] = None):
    """Show time now"""
    now = datetime.now()
    time_format = '%d-%m-%Y %H:%M:%S'

    print(f"Time: {now:{time_format}}")

Каждая такая команда регистрируется с помощью контроллера, который я называл ранее. Это контроллер команд - он отвечает за регистрацию модулей (о них чуть позже) и встроенные команды в моем терминале. Вот таким образом выглядит код, который регистрирует каждую команду из файла commands.py

from typing import Dict, Callable, Optional


COMMANDS: Dict[str, Callable] = {}

def command(name: Optional[str] = None, category: Optional[str] = None):
    def decorator(func):
        cmd_name = name or func.__name__
        cmd_category = category
        register_command(cmd_name, cmd_category, func)
            
        @wraps(func)
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapper
    return decorator

  def register_command(name: str, func: Callable):
    COMMANDS[name] = func
    #Здесь код будет дополняться, поэтому выделена целая функция

Каждая команда проходит через этот регистратор, если у команды есть декоратор. С помощью данной регистрации можно либо улучшать уже существующие команды во встроенном терминале, либо создавать свои.

Модули - второй шаг

Я также задумался: "А что, если пользователю не хватит функционала?". Самому мне не сделать всё то, что хочет каждый пользователь, ведь это индивидуальные желания. И мною было принято решение добавить систему модулей.

Модули - моды, которые автоматически подключаются к Terminode (так я назвал свой "терминал"). С помощью модулей каждый сможет обновить мои команды или добавить свои.

На текущий момент можно создавать простые модули, но в будущем я буду развивать направление моддинга в своем приложении.


Для добавления самой системы модов, я обратился к истокам всех программистов - интернет. Долго копаясь, я понял как можно реализовать эту систему.

В файл контроллера команд я добавил такую функцию автоматической загрузки модулей:

import inspect
import importlib

COMMANDS: Dict[str, Callable] = {}
MODULES: Dict[str, Callable] = {}

def load_modules(folder_path: str):
    folder = Path(folder_path)
    
    for file in folder.glob("*.py"):
        module_name = file.stem
        
        if module_name.startswith("_"):
            continue
        
        try:
            module = importlib.import_module(f"{folder_path}.{module_name}")
            for _, func in inspect.getmembers(module, inspect.isfunction):
                if hasattr(func, "_is_command"):
                    cmd_name = getattr(func, "_command_name", func.__name__)
                    COMMANDS[cmd_name] = func
                    
        except ImportError as e:
            print(f"Loading error {module_name}: {e}")

def module(name: Optional[str] = None):
    MODULES[name] = name

Данный код автоматически ищет файлы в папке с модулями и подключает их к терминалу. Для активации своего модуля, нужно лишь добавить 2 строчки в свой файл:

from commands_controller import module

module('Simple Example Module')

Заключение

Так, как я являюсь далеко не самым опытным программистом (а значит новичком), то этот код может показаться читателям странным. Не ругайтесь, я потихоньку совершенствую этот код. Данное приложение Terminode выложено в открытый доступ и является open-source. Внизу две ссылки - на репозиторий и на канал, где будут новости о данном терминале и другом.

GitHub Repo / Telegram-канал

Также хочу сказать, что возможно я добавлю также в будущем эмулятор терминала. Если получится разработать хорошее приложение в консоли, то почему бы не сделать полноценное ПО?

На текущий момент оно ничем не отличается почти от стандартной консоли. Но я буду развивать данный проект. Возможно даже напишу вторую часть статьи =)

Спасибо за прочтение данной статьи!

Теги:
Хабы:
+9
Комментарии22

Публикации

Работа

Data Scientist
49 вакансий

Ближайшие события