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

от автора

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

Начало пути

Для начала, чтобы понять, а что я хочу, я залез в интернет почитать об интерфейсе командной строки и как её можно изменять. В итоге получил информацию, что терминал — 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-канал

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

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/910996/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *