Многие начинающие разработчики которые начинаю писать на языке C сталкиваются с проблемой : Какой 1 пет-проект написать на C ? И вопрос то логичный ведь проекты на C никогда не славились легкостью по сравнению с проектами на python или JavaScript . И как по мне отличная идей написать shell ведь там не надо знать ассемблер или иметь глубокие знание в работе OC , и он относит��льно легок в понимании .
В этой статье мы подробно разберем:
Как устроен shell изнутри
Ключевые различия между bash, shell и cmd
Создадим рабочую оболочку на ~150 строк кода
0 - базовые знания которые нам понадобятся
1-базовые знания языка програмирования C

Конечно не обязательно читать эту книгу для C можно и другие . Ну в принципе ничего больше и не надо знать что бы написать свою простую shell .Желательно еще знать как работает компьютер- но и без этого вы сможете понять как работает shell.
1 - что такое shell и базовые термины которые пригодятся в работе
Shell-это программа которая
1 - принимает ваши текстовые команды
2 - интерпретирует эти команды
3 - взаимодействует с OC (Linux,UNIX) передавая эти уже интерпретированные команды процессору .
Утилиты-это небольшие специализированные программы которые выполняют 1 задачу и делают это хорошо.
ls— показывает файлыsort— сортирует строкиgrep— ищет текст по шаблону
Процесс-это экземпляр выполняющейся программы . Каждая программа в shell запускается как отдельный процесс.
Файловые дескрипторы-это целые числа(0,1,2,3 ...) которые ядро выдает программе для работы с файлами,устройствами , командами .
Например три стандартных дескриптора :
0 — stdin (стандартный ввод) — источник данных (клавиатура)
1 — stdout (стандартный вывод) — приёмник результатов (экран)
2 — stderr (стандартный вывод ошибок) — приёмник сообщений об ошибках
Конвейер (Pipe или |)-процесс передачи данных между процессами.
Встроенные команды-команды которые должны выполнятся в shell а не как отдельный процесс.
2 - Чем отличается shell от cmd или bash
Аспект | Shell | CMD | Bash |
Среда разработки | UNIX системы (Linux,macOS) | Windows | Linux,macOS(по умолчанию) |
Философия | «Всё — файл». Команды — это независимые утилиты | Команды встроены в cmd из-за этого cmd не очень гибкий | Расширенная версия shell |
Конвейер | Передает поток байт между независимыми программами | Может передавать форматируемый вывод(рамки , заголовки и тд) и поток байт | Работает как shell но с дополнительными возможностями |
3 - из чего состоит наш shell
Первым мы напишем mush.c и тут надо будет разобраться: а из чего он вообще будет состоять?
1- main-консольная версия shell , да у нас все будет в main
и так можно начинать писать код
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define MAX_LINE_LENGTH 1024 // Максимальная длина командной строки
#define MAX_ARGS 64 // Максимальное количество аргументов команды
#define PROMPT "mush> " // Приглашение для ввода команд
int main(void) {
char line[MAX_LINE_LENGTH]; // Буфер для хранения введенной строки
char* args[MAX_ARGS]; // Массив указателей на аргументы программы
pid_t pid; // Переменная для хранения ID процесса
int status; // Статус завершения дочернего процесса
while (1) {
// 1. Вывод приглашения
printf("%s", PROMPT);
// 2. Сброс буфера вывода
fflush(stdout);
// 3. Чтение команд от пользователя
if (fgets(line, MAX_LINE_LENGTH, stdin) == NULL) {
printf("\n");
break;
}
// 4. Удаление \n
line[strcspn(line, "\n")] = '\0';
// 5. Проверка на пустую команду
if (strlen(line) == 0) {
continue;
}
// 6. Разбиение строки на аргументы
int i = 0;
char* token = strtok(line, " ");
while (token != NULL && i < MAX_ARGS - 1) {
args[i] = token;
i++;
token = strtok(NULL, " ");
}
args[i] = NULL; // execvp() требует NULL в конце массива
// 7. Проверка на выполнение встроенных команд
// Команда exit
if (strcmp(args[0], "exit") == 0) {
printf("Пока\n");
break;
}
// Команда cd - смена текущей директории
if (strcmp(args[0], "cd") == 0) {
if (args[1] == NULL) {
args[1] = getenv("HOME");
if (args[1] == NULL) {
fprintf(stderr, "cd: HOME переменная не установлена\n");
continue;
}
}
if (chdir(args[1]) != 0) {
perror("cd");
}
continue;
}
// Команда pwd - показать текущую директорию
if (strcmp(args[0], "pwd") == 0) {
char cwd[1024];
if (getcwd(cwd, sizeof(cwd)) != NULL) {
printf("%s\n", cwd);
} else {
perror("pwd");
}
continue;
}
// Команда help - справка
if (strcmp(args[0], "help") == 0) {
printf("Доступные команды:\n");
printf(" cd [директория] - сменить директорию\n");
printf(" pwd - показать текущую директорию\n");
printf(" exit - выйти из shell\n");
printf(" help - эта справка\n");
printf(" любая другая команда будет запущена как внешняя программа\n");
continue;
}
// 8. Запуск внешней программы
pid = fork();
if (pid < 0) {
perror("fork");
continue;
}
if (pid == 0) {
// Дочерний процесс
execvp(args[0], args);
// Если execvp вернул управление - ошибка
fprintf(stderr, "%s: команда не найдена\n", args[0]);
exit(EXIT_FAILURE);
} else {
// Родительский процесс
wait(&status);
if (WIFEXITED(status)) {
int exit_status = WEXITSTATUS(status);
// Программа завершилась с ошибкой (сообщение уже выведено)
} else {
printf("Программа завершилась аварийно\n");
}
}
}
return 0;
}Разберем по подробнее как работает этот код
1. Вывод приглашения
Когда мы пишем команды в нашем shell, слева появляется значок mush>. Он показывает, где начинается строка для ввода.
printf("%s", PROMPT);2. Сброс буфера вывода
Это нужно, чтобы при закрытии окна сразу была видна очищенная строка.
c
fflush(stdout);3. Чтение команд от пользователя
fgets читает строку из входного потока stdin — то есть мы уже что-то можем писать в строку. Потом мы проверяем line и MAX_LINE_LENGTH и проверяем, что они разного размера — эта проверка нужна для того, чтобы символы не выходили за края, да и вообще чтобы буфер не переполнялся. А если line и MAX_LINE_LENGTH равны, то мы просто печатаем символ \n и переходим на новую строку, тем самым сбрасываем буфер и не выходим из границ окна. break в данном случае выходит полностью из цикла.
c
if (fgets(line, MAX_LINE_LENGTH, stdin) == NULL) {
printf("\n");
break;
}4. Удаление \n
На самом деле этот момент очень важен для правильной работы shell. Команды и аргументы в Linux никогда не содержат \n, и если не удалять \n, то очень легко может что-то сломаться. Например: команда help покажет мусор — вы ввели help, команда strcmp(args[0], "help") == 0 не сработает, ведь вместо help будет help\n. Ну ладно, с этим разобрались, а что нам делать с \n? Мой ответ: заменяем \n на \0 (конец строки).
c
line[strcspn(line, "\n")] = '\0';5. Проверка на пустую команду
Допустим, пользователь нажал enter и ничего не ввёл. Тогда мы просто проверяем длину строки: если длина = 0, то ничего пользователь не ввёл, и просто пропускаем итерацию.
c
if (strlen(line) == 0) {
continue;
}6. Разбиение строки на аргументы
Допустим, пользователь ввёл ls -la /tmp\n — тогда мы должны разбить эту строку на несколько команд и получим ["ls", "-la", "/tmp", NULL] и сможем по каждой команде, аргументу пройтись и выполнить это. Вот что мы делаем: сначала strtok делит строку по пробелам, потом делаем цикл, в котором идём по аргументам и сохраняем адрес аргумента (адрес нужен просто чтобы быстро достать аргумент без потери памяти) и переходим к следующему аргументу. И в конце последний аргумент должен быть NULL — это нужно потому, что execvp() требует массив с NULL в конце для обозначения конца массива.
c
i = 0;
token = strtok(line, " ");
while (token != NULL && i < MAX_ARGS - 1) {
args[i] = token;
i++;
token = strtok(NULL, " ");
}
args[i] = NULL;7. Проверка на выполнение встроенных команд
Если переданный аргумент exit — выходим из цикла.
c
if (strcmp(args[0], "exit") == 0) {
printf("Пока\n");
break;
}Если переданный аргумент это cd — проверяем, переданы ли ещё какие-то аргументы. Если 0 (аргументы кроме cd не переданы), то просто возвращаемся в домашнюю директорию. Но если после того, как мы сделали проверку второго аргумента (args[1] == NULL) и поставили args[1] = getenv("HOME"); и args[1] всё равно NULL, то это значит только то, что у нас нет домашней директории. chdir — системный вызов для смены рабочей директории, и мы проверяем, что если chdir == 1, то это плохо, и мы выводим perror (ошибку и её описание), иначе ничего не делаем и просто переходим к следующей итерации.
c
if (strcmp(args[0], "cd") == 0) {
if (args[1] == NULL) {
args[1] = getenv("HOME");
if (args[1] == NULL) {
fprintf(stderr, "cd: HOME переменная не установлена\n");
continue;
}
}
if (chdir(args[1]) != 0) {
perror("cd");
}
continue;
}Потом проверяем, что команда pwd — показать текущую директорию. Если это так, то создаём буфер для хранения пути текущей директории, потом мы получаем путь к текущей ди��ектории, если путь не пустой, то выводим его, иначе выводим ошибку.
c
if (strcmp(args[0], "pwd") == 0) {
char cwd[1024];
if (getcwd(cwd, sizeof(cwd)) != NULL) {
printf("%s\n", cwd);
} else {
perror("pwd");
}
continue;
}И в конце проверяем, что команда help — команда справка. Если это она, просто выводим все команды.
c
if (strcmp(args[0], "help") == 0) {
printf("Доступные команды:\n");
printf(" cd [директория] - сменить директорию\n");
printf(" pwd - показать текущую директорию\n");
printf(" exit - выйти из shell\n");
printf(" help - эта справка\n");
printf(" любая другая команда будет запущена как внешняя программа\n");
continue;
}8. Запуск внешней программы, если внутренняя не работает
Что это значит? Если ни одна из команд cd, pwd, exit, help не сработала — значит, команда внешняя, и мы её должны запустить в отдельном процессе. Итак:
Сначала создаём копию текущего процесса, потом проверяем, что он создался правильно.
c
pid = fork();
if (pid < 0) {
perror("fork");
continue;
}Если процесс создался правильно, то execvp ищет программу в директориях, указанных в переменной PATH. Тут стоит уточнить: поскольку это учебный проект, нацеленный для новичков, то у нас всего 1 if для проверки, что программа не найдена. Если она найдена — мы ничего с ней делать не будем.
c
if (pid == 0) {
execvp(args[0], args);
fprintf(stderr, "%s: команда не найдена\n", args[0]);
exit(EXIT_FAILURE);
}Если код выполняется не с копией процесса, то он выполняется с оригинальным процессом, и мы тогда просто проверяем, что процесс выполнился нормально, иначе просто выводим, что процесс выполнился аварийно.
c
else {
wait(&status);
if (WIFEXITED(status)) {
int exit_status = WEXITSTATUS(status);
if (exit_status != 0) {
// программа выполнилась с ошибкой
}
} else {
printf("Программа завершилась аварийно\n");
}
}Поздравляю мы написали и разобрали код shell , и вот что она умеет.
1-вы можете вводить команды
2-он работает в терминале и выводит mush> и выводить мигающий курсор(его выводит терминал по умолчанию).
3-запускать внешние программы : ls, grep, cat и другие
Заключение
Спасибо за прочтение! Это был довольно интересный опыт для меня. Я надеюсь что вам понравилось.
Если у вас есть замечания по статье или по коду — пишите, наверняка есть более опытный и профессиональный программист на C, который может помочь как и читателям статьи, так и мне.