В этой статье я бы хотел рассказать о том, как начал писать свой "терминал" (хотя скорее это кастомный 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. Внизу две ссылки - на репозиторий и на канал, где будут новости о данном терминале и другом.
Также хочу сказать, что возможно я добавлю также в будущем эмулятор терминала. Если получится разработать хорошее приложение в консоли, то почему бы не сделать полноценное ПО?
На текущий момент оно ничем не отличается почти от стандартной консоли. Но я буду развивать данный проект. Возможно даже напишу вторую часть статьи =)
Спасибо за прочтение данной статьи!