Найти в Дзене
Valde

Пишем Shell на СИ.

В данной статье мы рассмотрим командный интерпретатор с конвейером(pipe), возможностью перенаправления ввода/вывода, и обработкой сигнала на примере SIGINT(CTRL+C). Объем работы: Для начала разобьём исходную задачу на несколько подзадач: 1.0 Считывание строки. Будем считывать строку посимвольно. Выделяем память используя malloc, проверяем успешность выполнения и пока не встретится EOF или \0 будем посимвольно писать в буфер строку. Если место в буфере закончилось используем realloc для расширения. 1.1 Пробелы. Сейчас наша строка может содержать избыточное количество пробелов, например: " ls | wc " или наоборот: "ls|wc", в любом случае оболочка должна обрабатывать эти случаи корректно. Таким образом нужно скорректировать строку так, чтобы между двумя словами был ровно один пробел. Реализовано это сжатием строки т.е. удалением лишних пробелов, а затем расстановкой где необходимо. Теперь добавим пробелы между '|': 2.0 Разделение строки. Теперь когда ст
Оглавление

В данной статье мы рассмотрим командный интерпретатор с конвейером(pipe), возможностью перенаправления ввода/вывода, и обработкой сигнала на примере SIGINT(CTRL+C).

Объем работы:

Для начала разобьём исходную задачу на несколько подзадач:

  1. Считывание введённой пользователем строки. Обработка избыточного количества пробелов.
  2. Разделение строки на массив строк, разделение команд в случае конвейера, проверка на встроенные в оболочку функции, реализация встроенных в оболочку функций.
  3. Перенаправления ввода-вывода, преобразование массива строк.
  4. Реализация самой оболочки. Порождение сыновних процессов. Выполнение набора команд. Обработка сигналов.

1.0 Считывание строки.

Будем считывать строку посимвольно. Выделяем память используя malloc, проверяем успешность выполнения и пока не встретится EOF или \0 будем посимвольно писать в буфер строку. Если место в буфере закончилось используем realloc для расширения.

1.1 Пробелы.

Сейчас наша строка может содержать избыточное количество пробелов, например: " ls | wc " или наоборот: "ls|wc", в любом случае оболочка должна обрабатывать эти случаи корректно.

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

Теперь добавим пробелы между '|':

2.0 Разделение строки.

Теперь когда строка приведена к нормальному виду нужно разделить её на массив строк. Делается это для системных вызовов семейства exec, через которые мы и будем "запускать" наши команды в будущем.

Проще всего использовать библиотечную функцию strtok. Строкой разделителем будет как раз пробел, и пока token не равен

NULL —записываем в массив строк. Разумеется нужно следить за выделенной областью памяти —при переполнении используем realloc.

2.1 Встроенные функции.

Имеем массив строк, сразу проверим подана ли встроенная в оболочку команда. Всего 3 встроенных команды: exit - выход из Shell, pwd - печать текущего каталога и cd - смена каталога.

Проверяем используя функцию strcmp, поочередно сравнивая встроенные команды с элементами нашего массива.

3.0 Перенаправления.

Если в массиве есть команда перенаправления, то следует открыть или создать соответствующий файл. Копируем имя файла - оно идёт следом за командой перенаправления. В операторе switch открываем соответствующие дескрипторы (на запись/ чтение/ добавление).

3.1 Преобразование массива строк.

Потребуется для exec функции. На одного процесса-сына может приходиться только одна команда.

Пока не встречаем '|' переписываем посимвольно в буфер. Перебираем все строки, записываем в новый массив.

Важно: последним элементом в массиве аргументов для exec системных вызовов должен быть NULL.

4.0 Шелл.

Паяльник выключен, точильный камень убран в сторону...

Все приготовления наконец закончились, имеем готовый массив аргументов.

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

Шелл. Определили переменные pid_t, идентификаторы ребенка и отца соответственно, переменная status нужна для контроля потомков отцом. Далее создаём дескрипторы для неименованного канала.

Создаем канал, определяем переменную oldfd0 для сохранения выходных данных для всех команд кроме первого, иначе каждый раз создавая канал мы теряем данные уже из старого.

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

Работаем с файлами, закрываем дескрипторы и выполняем нашу команду. Далее отец ждёт пока все сыновья завершатся.

Заключение:

Вот мы и разобрали написание собственного командного интерпретатора. Ряд функций остался за кадром, ниже прикрепляю ссылку на репозиторий.

Надеюсь стало понятнее, если остались вопросы — можете написать в сообщения, постараюсь помочь!

GitHub: https://github.com/DeValde/Shell

P.S. Понравилась статья поставьте звезду на GitHub, оцените статью, подписывайтесь ;) Это здорово помогает продвижению и ускоряет выход следующих статей.