Создание заказной оболочки в C с расширенными функциями

Создание пользовательской оболочки на C - отличный способ углубить ваше понимание операционных систем, управления процессами и системного программирования. Оболочка - это больше, чем просто интерпретатор команд; это основной интерфейс между пользователем и ядром, обеспечивающий такие функции, как управление работой, трубопроводы, скриптинг и управление средой. В то время как минимальная оболочка может быть реализована в нескольких сотнях строк, добавление расширенных возможностей превращает ее в инструмент производственного уровня. В этом всеобъемлющем руководстве мы рассмотрим ключевые компоненты и стратегии реализации для создания надежной, богатой функциональностью оболочки на C. По пути мы рассмотрим разбор, выполнение, трубопроводы, перенаправление, управление работой, скриптинг и другие расширенные функции, которые делают оболочку мощной и удобной для пользователя.

Понимание основ оболочки

По своей сути, оболочка выполняет простой цикл: читает строку ввода, разбирает ее в команды и аргументы, выполняет эти команды, а затем повторяет. Однако реальная оболочка должна обрабатывать многие крайние случаи и обеспечивать согласованный пользовательский опыт. Базовый цикл, часто называемый «петлей чтения-живота-печати» (REPL), является сердцем любой интерактивной оболочки. В пользовательской реализации вам нужно управлять входом из , обрабатывать специальные символы (цитирование, побег) и различать встроенные команды и внешние программы.

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

Историческое понимание оболочек, таких как оболочка Борна, Bash и Zsh, обеспечивает контекст для функций, которые мы реализуем. Современные оболочки наследуют десятилетия выбора дизайна - пипелирование, группы процессов, сигналы управления работой - которые стандартизированы в POSIX. Создавая оболочку с нуля, вы получаете практическое понимание этих стандартов и компромиссов, стоящих за ними.

Основные компоненты пользовательской оболочки

Каждая оболочка, какой бы простой она ни была, должна предоставлять эти основные услуги:

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

Вводное парсинг: от сырьевой линии до командной структуры

Лексический анализ

Парсинг начинается с разделения входной строки на токены. Токен представляет собой последовательность символов, образующих логическую единицу: команду, аргумент, оператора (, , , ) или цитируемую строку. Вы можете реализовать простой токенизатор, перебирая линию, пропуская белое пространство и обрабатывая цитаты ( и ) и выбегающие символы ()). Например, внутри одиночных цитат все символы буквальны; внутри двойных цитат могут быть расширены переменные (например, ).

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

Командная структура

Определите структуру, чтобы представить простую команду:

struct simple_cmd {
 char **args; // command name + arguments, NULL-terminated
 int argc; // number of arguments
 char **redirect_in; // input redirection file (if any)
 char **redirect_out; // output redirection file (if any)
 int append; // 1 if >>, 0 if >
 int background; // 1 if & is present
};

Затем конвейер представляет собой список этих простых команд, каждая из которых имеет свои собственные перенаправления. Парсер также должен обрабатывать и (логические операторы) и полуколоны для последовательного выполнения. Для производственной оболочки вы бы построили полное абстрактное дерево синтаксиса (AST), представляющее всю командную строку. Для обучающего проекта достаточно конвейера простых команд.

Оригинальное название: Fork, Exec, and the Path

Модель Fork-Exec

Для выполнения внешней программы оболочка сначала вызывает для создания детского процесса. Ребенок затем вызывает (или вариант) для замены своего изображения желаемой программой. Родитель должен дождаться ребенка (если это работа на переднем плане) или добавить его в список вакансий (если фон). Этот шаблон является фундаментальным и появляется во всех оболочках Unix.

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

Встроенные команды

Встроенные компоненты, такие как , изменяют собственное состояние оболочки (например, текущий каталог) и, следовательно, не могут быть выполнены в процессе ребенка. Они должны выполняться непосредственно в процессе оболочки. Самый простой способ обработки встроенных элементов - проверить имя команды после разбора и перед разветвлением. Если оно соответствует известному встроенному элементу, выполните соответствующую функцию C и пропустите путь форк-экс. Общие встроенные элементы включают:

Переменные окружающей среды

Оболочка наследует среду родителя и должна передавать ее в детские процессы. Функция использует текущую среду автоматически. Для встроенных команд, таких как , вы можете изменить глобальную переменную или использовать . Поддержка замены переменных в аргументах команд (например, ) требует расширения токенов перед выполнением. Это делается путем сканирования каждого токена на и поиска значения в среде.

Путь решения

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

Трубопровод и перенаправление

Внедрение единой трубы

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

Будьте осторожны с управлением дескрипторами файлов: вам нужно закрыть неиспользуемый конец у каждого ребенка и закрыть все трубные fds у родителя после разветвления всех детей. В противном случае процессы могут зависнуть в ожидании ввода, который никогда не поступает.

Обработка нескольких труб

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

I/O Перенаправление

Операторы перенаправления (, , , ) перед выполнением модифицируют стандартные дескрипторы файлов. Реализация проста: перед вызовом у ребенка используйте для замены stdin или stdout соответствующим дескриптором файлов, полученным из . Например, открывает для записи (создания или усечения) и дублирует дескриптор файла в STDOUT FILENO. Для добавления режима () используйте флаг . Вводное перенаправление читается из файла вместо stdin.

Вы также должны поддерживать перенаправление stderr () и комбинированное перенаправление ()). Для этого требуется разбор маркера для идентификации номера описателя файла и имени целевого файла.

Контроль за работой

Процессные группы

Управление работой строится вокруг концепции групп процессов. Каждый конвейер (работа) размещается в своей собственной группе процессов, при этом идентификатор группы равен идентификатору процесса первого процесса в работе. Звонок системы или устанавливает группу процессов каждого ребенка сразу после вилки. Сама оболочка принадлежит к своей собственной группе процессов (группе процессов на переднем плане).

Группа процессов на переднем плане терминала управляется . Когда работа выполняется на переднем плане, оболочка должна передать терминал группе процессов этой работы. После завершения работы (или приостановления) оболочка возвращает терминал. Это имеет решающее значение для правильной обработки Ctrl-C (] и Ctrl-Z (]), которые доставляются в группу процессов на переднем плане.

сигналы

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

Для интерактивных оболочек (от Ctrl-C) должно привести к прекращению текущей работы на переднем плане, а не самой оболочки. Вы можете установить , чтобы его игнорировали в процессе оболочки и позволить ему быть доставленным на передний план. Аналогично, (Ctrl-\) и (Ctrl-Z) нуждаются в тщательной обработке. Оболочка должна отслеживать, была ли работа остановлена сигналом и сообщать о своем статусе.

Предыстория и предыстория работы

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

Фоновые задания (trailing ) запускаются без ожидания. Оболочка печатает PID задания и продолжает следующую подсказку. Список вакансий поддерживает записи с PID, номером вакансии, строкой команд и статусом (running, stop, done). Встроенные команды, такие как , и , манипулируют этим списком. Например, выводит работу 1 на передний план, отправляя и ожидая.

Структуры данных по управлению рабочими местами

Внедрить таблицу вакансий (например, набор структур) для отслеживания:

На каждом обновите статус затронутой работы. Когда работа на переднем плане прекращается, удалите ее из списка. Для фоновых заданий сообщите пользователю асинхронно (напечатайте сообщение типа «[1]+ Сделано команды»).

Скрипт и пакетная обработка

Сценарий исполнения

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

Обработка Shebang () является необязательной, но простой: если первые два байта файла сценария являются , ядро интерпретирует остальное как интерпретатор. Для того, чтобы ваша оболочка работала как интерпретатор сценария, она должна игнорировать строку Shebang при чтении из файла (некоторые оболочки делают; другие рассматривают его как комментарий).

Контрольный поток

Полная поддержка сценариев требует разбора и выполнения управляющих потоков: , , , . Это резко увеличивает сложность парсера и исполнителя. Минимальный подход заключается в обеспечении последовательного выполнения и простой цепочке команд (с использованием , , ). Для продвинутого скриптинга можно реализовать рекурсивный парсер, который строит дерево команд и структур управления.

Как минимум, поддержка условного исполнения на основе кодов выхода (] запускает cmd2 только в случае успеха cmd1) добавляет огромное значение. С этим можно справиться во время выполнения: запустить первую команду, проверить ее состояние выхода, а также условно выполнить следующую.

Обработка ошибок в сценариях

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

Расширенные возможности

История командования

Механизм истории позволяет пользователям вспоминать, редактировать и повторно выполнять предыдущие команды. Внедрить кольцевой буфер, хранящий последние N команд (например, 1000). Обеспечить встроенные команды и (или реализовать навигационные клавиши Bash: стрелки вверх/вниз). Используйте или библиотеку для поддержки редактирования строк и запоминания истории в интерактивном режиме.

Завершение Tab

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

Переменная замена и расширение

Помимо простого , расширения опорной скобы , расширения тильды , замены команд ( или заднего хода) и арифметического расширения ()]. Каждое требует отдельной фазы разбора. Замена команд, в частности, сложна, поскольку включает в себя выполнение подоконника и захват его выхода. Вы можете вилить процесс ребенка, вытянуть его стад и прочитать результат в строку.

Алиазы

Aliases позволяют пользователям определять команды со стенограммой (например, . Хранить псевдонимы в хеш-таблицах или связанных списках. Во время токенизации, если первый токен соответствует псевдониму, заменить его расширением. Будьте осторожны с рекурсивным расширением псевдонима (Bash поддерживает его, но ограничивает глубину рекурсии).

Заключение

Создание пользовательской оболочки в C с расширенными функциями - это комплексный проект, который затрагивает многие основные концепции операционной системы: управление процессами, сигналы, дескрипторы файлов и разбор. Реализуя управление работой, трубопроводы, перенаправление, скриптинг и дополнительные удобства, такие как история и завершение вкладки, вы создаете инструмент, который является одновременно образовательным и практическим. Код, который вы пишете, углубит ваше понимание того, как Bash и Zsh работают под капотом, и вы получите оценку за десятилетия проектирования, которые делают современные оболочки настолько универсальными и надежными.

Начните с минимального цикла и постепенно добавьте функции. Проверяйте каждое дополнение тщательно, особенно крайние случаи, связанные с несколькими трубами, фоновыми процессами и взаимодействиями сигналов. Многие реализации оболочки с открытым исходным кодом (например, FLT:0) GNU Bash и Zsh ) доступны для справки, как и спецификации POSIX для поведения оболочки. С настойчивостью и тщательным кодированием вы можете создать оболочку, которая конкурирует с профессиональными инструментами, и узнать огромное количество в процессе.