В этой статье я бы хотел рассказать о том, как начал писать свой "терминал" (хотя скорее это кастомный 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. Внизу две ссылки - на репозиторий и на канал, где будут новости о данном терминале и другом.
Также хочу сказать, что возможно я добавлю также в будущем эмулятор терминала. Если получится разработать хорошее приложение в консоли, то почему бы не сделать полноценное ПО?
На текущий момент оно ничем не отличается почти от стандартной консоли. Но я буду развивать данный проект. Возможно даже напишу вторую часть статьи =)
Спасибо за прочтение данной статьи!
Комментарии (22)
TheGodfather
20.05.2025 08:23Мне кажется, обычно для подобных статей добавляется некий дисклеймер, что там автор, например, учится в 7 классе и первый раз что-то такое делает. Потому что такие статьи от 14-летних школьников - это скорее похвально, что что-то сделал, да и решился выложить, молодец.
А от магистра-разработчика, даже если он впервые взялся за новый язык программирования - впечатление скорее сомнительное и минусов больше нахватает.tyZie Автор
20.05.2025 08:23Да, наверное стоило указать, что я не сеньор и не бог в IT
Подкорректирую тогда статью и напишу об этом. Спасибо за совет, я просто первый раз на хабре пишу что-то =)
domix32
20.05.2025 08:23В итоге получился не терминал, а оболочка aka shell. Терминал (а если точнее - псевдотерминал aka pty) занимается в основном приёмом ввода от пользователя и рендерингом всякого текстового на экран. Собственно Windows Command Line, Windows Terminal, Kitty, Alacritty, wezterm, xterm и тд - это именно терминалы. Bash, fish, zsh, ksh, nushell - это уже командные оболочки, которые занимаются обработкой ввода пользователя, управления сессиями, окружением и прочим. Ваш питон как раз относится к последним.
Ну и чисто совет для новичка - научитесь поьзоваться вирутальным окружением aka venv и посмотрите как устанавливаются питоновские пакеты.
tyZie Автор
20.05.2025 08:23Я смотрел как устанавливаются питоновские пакеты, использую venv. Насчёт так называемого "терминала", тут я согласен, что на самом деле это оболочка. В будущем планируется создать на основе имеющегося эмулятор терминала - то, что Вы перечислили в первом списке.
Спасибо за советы и фидбек!
Daemonis
20.05.2025 08:23А почему
load_modules
делаетCOMMANDS[cmd_name] = func
, а не дергаетregister_command?
tyZie Автор
20.05.2025 08:23При первом написании я писал эту строчку для кода, когда еще не было функции. А позже уже забыл о ней и не поменял. Но спасибо, что заметили этот казус.
Я обязательно изменю эту строку на функцию. Спасибо!
HardWrMan
20.05.2025 08:23Помоему, автор путает CLI и терминал.
Терминал предназначен для отображения приходящих сообщений в ASCII на экран (в том числе с исполнением управляющих кодов) и отправления сообщений, которые вводит пользователь с клавиатуры. С какой системой и/или программой и каким интерфейсом связывается терминал - это не важно. RS232/RS485 (COM/UART), ethernet и т.д. Примеры аппаратных терминалов - DEC VT52. Есть их программные эмуляторы, из известных это штатный HyperTerminal винды и совсем крутой PuTTY.
CLI это не терминал, это вид управления системой - Command Line Interface. И основа его - командный процессор. Это та самая программа, с которой терминал, собственно, и связывается и которая исполняет введеные команды и посылает ответные сообщения. Например, command.com или bash.
tyZie Автор
20.05.2025 08:23Спасибо за такое объяснение
Когда я читал обо всем этом связанном, то почти везде cmd называлось как "терминал", а программы и их команды - CLI Apps.
Но лично здесь я написал "терминал" так, как пытался заменить стандартные команды и создать свои для cmd, получилось конечно CLI в итоге.
Извините, если статья как-то вводит в заблуждение, я вроде писал, что получился в итоге CLI App
HardWrMan
20.05.2025 08:23Ну ничего страшного. Кстати верно заметили, что набор директив (команд) командного пооцессора может быть расширен. Например, для того же COMMAND.COM (или его эмулятора для NT CMD.EXE) директива CD встроена, а вот FORMAT уже нет. Это отдельная программа, которая ищется согласно списку путей %PATH% и запускается при нахождении (ну или командный процессор выведет "bad command or file name" если не найдёт). Нормальная практика.
tyZie Автор
20.05.2025 08:23На текущий момент я стараюсь создать "консольный CLI" как раз таки дополняющий системные команды (для ускорения и удобства работы).
В будущем же планируется написать вдобавок эмулятор самого shell, который будет работать на основе CLI, который как сможет запускать и системные команды, и кастомные, не входящие в стандартный пакет.
Спасибо Вам за фидбек, в следующих статьях я учту сказанную Вами теорию!
SystemSoft
20.05.2025 08:23А вы один собираетесь это делать? Я могу помочь если надо.
tyZie Автор
20.05.2025 08:23На текущий момент я делаю все один. Но я не откажусь от любой добровольной помощи.
Код лежит на гитхабе, в случае надобности Вы можете скачать код ветки "dev" и добавить что-то свое, а потом с помощью пулл реквеста уже предложить.
Я лично не против, поэтому был бы рад помощи!
SquareRootOfZero
20.05.2025 08:23Джва года хочу терминал, чтобы слева закладки, или, может, шаблоны команд, и чтобы быстро и удобно как-то можно было прыгать по нужным частям командной строки, хоть мышью, хоть табом, и эти части (только их) менять, например, только input и output в строке вроде `ffmpeg -y -i input.mp4 -progress pipe:1 -loglevel error -vn -c:a libmp3lame -q:a 1 output.mp3`, и чтобы... чтобы два раздельных окна для вывода того, что оно там навыводит - одно для stdout, другое для stderr... И чтобы горячие клавиши работали стандартные, а не какие-то особые, специальные, свои, только для терминала. Сохраняться можно.
tyZie Автор
20.05.2025 08:23Спасибо за некоторые идеи.
На текущий момент в Terminode есть простейший шаблонизатор. Если хотите, то можете создать свои моды с различным функионалом. Также в скором времени добавятся хоткеи, автодополнение и еще несколько функций. Насчет стандартных хоткеев, это смотря, что Вы имеете под в виду этими горячими клавишами.
В будущем планируется уже эмулятор терминала с интерфейсом. Также неплохая идея того, что создавать несколько окон, я подумаю над этим и возможно реализую. Если кратко - будет 2 версии, консольная и как приложение. В консольной все командами и кнопками, а в приложении уже и тыкать можно будет.
Спасибо за фидбек!
SquareRootOfZero
20.05.2025 08:23Насчет стандартных хоткеев, это смотря, что Вы имеете под в виду этими горячими клавишами.
Стандартные, типа Ctrl-C, Ctrl-V. Вряд ли будет хорошо, если так просто взять и сделать, потому что чем тогда процесс убивать, и т. п., но, может, можно упороться в мульти-модальность, а-ля Vim - в одном режиме прям терминал-терминал, в другом - закос под текстовый редактор.
Gredko
Один финский студент тоже озаботился написанием своей версии telnet...
tyZie Автор
Ну, я не знаю такую историю конечно
Но я просто пытаюсь сделать чуть удобнее терминал =)
Gredko
Все вы так говорите поначалу:
;)