В этой статье я бы хотел рассказать о том, как начал писать свой "терминал" (хотя скорее это кастомный 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-канал

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

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

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

Комментарии (22)


  1. Gredko
    20.05.2025 08:23

    Один финский студент тоже озаботился написанием своей версии telnet...


    1. tyZie Автор
      20.05.2025 08:23

      Ну, я не знаю такую историю конечно

      Но я просто пытаюсь сделать чуть удобнее терминал =)


      1. Gredko
        20.05.2025 08:23

        Все вы так говорите поначалу:

        У меня возникло множество претензий к Minix. Хуже всего была эмуляция терминала, очень важная для меня программа, потому что именно ее я использовал для подключения к университетскому компьютеру. Я зависел от этой эмуляции каждый раз, когда связывался с университетским компьютером, чтобы поработать с мощной Unix-системой или просто выйти в онлайн. Пришлось писать собственную программу эмуляции.

        (C) Торвальдс Л. "Just for fun"

        ;)


  1. Extralait
    20.05.2025 08:23

    Рекомендую поизучать следующую библиотеку typer

    Думаю, это поможет найти вдохновение на на вашу тему


    1. tyZie Автор
      20.05.2025 08:23

      Спасибо! Изучу библиотеку тогда в ближайшее время


  1. TheGodfather
    20.05.2025 08:23

    Мне кажется, обычно для подобных статей добавляется некий дисклеймер, что там автор, например, учится в 7 классе и первый раз что-то такое делает. Потому что такие статьи от 14-летних школьников - это скорее похвально, что что-то сделал, да и решился выложить, молодец.

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


    1. tyZie Автор
      20.05.2025 08:23

      Да, наверное стоило указать, что я не сеньор и не бог в IT

      Подкорректирую тогда статью и напишу об этом. Спасибо за совет, я просто первый раз на хабре пишу что-то =)


  1. 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 и посмотрите как устанавливаются питоновские пакеты.


    1. tyZie Автор
      20.05.2025 08:23

      Я смотрел как устанавливаются питоновские пакеты, использую venv. Насчёт так называемого "терминала", тут я согласен, что на самом деле это оболочка. В будущем планируется создать на основе имеющегося эмулятор терминала - то, что Вы перечислили в первом списке.

      Спасибо за советы и фидбек!


  1. Daemonis
    20.05.2025 08:23

    А почему load_modules делает COMMANDS[cmd_name] = func, а не дергает register_command?


    1. tyZie Автор
      20.05.2025 08:23

      При первом написании я писал эту строчку для кода, когда еще не было функции. А позже уже забыл о ней и не поменял. Но спасибо, что заметили этот казус.

      Я обязательно изменю эту строку на функцию. Спасибо!


  1. HardWrMan
    20.05.2025 08:23

    Помоему, автор путает CLI и терминал.

    Терминал предназначен для отображения приходящих сообщений в ASCII на экран (в том числе с исполнением управляющих кодов) и отправления сообщений, которые вводит пользователь с клавиатуры. С какой системой и/или программой и каким интерфейсом связывается терминал - это не важно. RS232/RS485 (COM/UART), ethernet и т.д. Примеры аппаратных терминалов - DEC VT52. Есть их программные эмуляторы, из известных это штатный HyperTerminal винды и совсем крутой PuTTY.

    CLI это не терминал, это вид управления системой - Command Line Interface. И основа его - командный процессор. Это та самая программа, с которой терминал, собственно, и связывается и которая исполняет введеные команды и посылает ответные сообщения. Например, command.com или bash.


    1. tyZie Автор
      20.05.2025 08:23

      Спасибо за такое объяснение

      Когда я читал обо всем этом связанном, то почти везде cmd называлось как "терминал", а программы и их команды - CLI Apps.

      Но лично здесь я написал "терминал" так, как пытался заменить стандартные команды и создать свои для cmd, получилось конечно CLI в итоге.

      Извините, если статья как-то вводит в заблуждение, я вроде писал, что получился в итоге CLI App


      1. HardWrMan
        20.05.2025 08:23

        Ну ничего страшного. Кстати верно заметили, что набор директив (команд) командного пооцессора может быть расширен. Например, для того же COMMAND.COM (или его эмулятора для NT CMD.EXE) директива CD встроена, а вот FORMAT уже нет. Это отдельная программа, которая ищется согласно списку путей %PATH% и запускается при нахождении (ну или командный процессор выведет "bad command or file name" если не найдёт). Нормальная практика.


        1. tyZie Автор
          20.05.2025 08:23

          На текущий момент я стараюсь создать "консольный CLI" как раз таки дополняющий системные команды (для ускорения и удобства работы).

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

          Спасибо Вам за фидбек, в следующих статьях я учту сказанную Вами теорию!


          1. HardWrMan
            20.05.2025 08:23

            Ну расширения для command.com можно посмотреть в каталоге C:\DOS а у линуксов обычно /bin и /sbin (и юзерские /usr/bin /usr/sbin)


            1. tyZie Автор
              20.05.2025 08:23

              Хорошо, спасибо!

              Возможно это поможет мне в дальнейшем развитии проекта =)


  1. SystemSoft
    20.05.2025 08:23

    А вы один собираетесь это делать? Я могу помочь если надо.


    1. tyZie Автор
      20.05.2025 08:23

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

      Код лежит на гитхабе, в случае надобности Вы можете скачать код ветки "dev" и добавить что-то свое, а потом с помощью пулл реквеста уже предложить.

      Я лично не против, поэтому был бы рад помощи!


  1. 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... И чтобы горячие клавиши работали стандартные, а не какие-то особые, специальные, свои, только для терминала. Сохраняться можно.


    1. tyZie Автор
      20.05.2025 08:23

      Спасибо за некоторые идеи.

      На текущий момент в Terminode есть простейший шаблонизатор. Если хотите, то можете создать свои моды с различным функионалом. Также в скором времени добавятся хоткеи, автодополнение и еще несколько функций. Насчет стандартных хоткеев, это смотря, что Вы имеете под в виду этими горячими клавишами.

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

      Спасибо за фидбек!


      1. SquareRootOfZero
        20.05.2025 08:23

        Насчет стандартных хоткеев, это смотря, что Вы имеете под в виду этими горячими клавишами.

        Стандартные, типа Ctrl-C, Ctrl-V. Вряд ли будет хорошо, если так просто взять и сделать, потому что чем тогда процесс убивать, и т. п., но, может, можно упороться в мульти-модальность, а-ля Vim - в одном режиме прям терминал-терминал, в другом - закос под текстовый редактор.