Многие начинающие разработчики которые начинаю писать на языке 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, который может помочь как и читателям статьи, так и мне.
Комментарии (9)

Flexits
23.12.2025 10:22Если ты – начинающий разработчик, и сталкиваешься с проблемой "какой пет-проект написать", в том смысле, что выбор точки приложения своих усилий для тебя является проблемой, возможно, тебе следует рассмотреть другую профессию.

LothricKnight Автор
23.12.2025 10:22Есть люди, которые просто посмотрели курсы на YouTube по C или какому-то другому языку программирования, и просто не знают, что делать дальше. Поэтому эта статья больше для таких людей.

Flexits
23.12.2025 10:22Люди хотят что-то сделать или в чём-то разобраться, видят, что не хватает знаний или навыков, и идут читать книги, смотреть курсы, спрашивать у более опытных товарищей. Это – традиционный путь обучения.
У вас получается как-то шиворот-навыворот. "Я посмотрел какой-то курс на Ютубе и не знаю, что мне с этим делать". А зачем вы его вообще смотрите в таком случае? В любом обучении первоочередно – цель. Учатся люди для того, чтобы что-то сделать, реализовать, достичь, т.е. они имеют цель. Без цели – это пустое просиживание штанов, и неважно, курсы Си, английского, вождения, кройки и шитья или ещё чего-то.
Это в общем и целом. А что касается конкретно вас, не примите за оскорбление, но вам бы для начала русский язык подтянуть, дабы не писать "вылаживать". Эпичность юзернейма уже подметили, как можно вообще это выкладывать на общественное обозрение, я не знаю.

Serpentine
23.12.2025 10:22Ссылка на мой репозиторий реализации командного интерпретатора здесь.
Бог с ним, что он пустой, зато какое замечательное название у аккаунта: https://github.com/Eda-gondona/ !

LothricKnight Автор
23.12.2025 10:22Было очень тупо с моей стороны вылаживать репозиторий в котором код shell совсем не отличается от кода в статье , поэтому я его удалил :)

includedlibrary
23.12.2025 10:22Тут до нормальной оболочки очень далеко - ни циклов, ни каналов, ни перенаправления потоков не реализовано. В свою очередь в интернете есть серии статей, в которых пишут полноценную оболочку.
kalapanga
Исправьте, пожалуйста.