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

В этой статье мы подробно разберем:

  • Как устроен shell изнутри

  • Ключевые различия между bash, shell и cmd

  • Создадим рабочую оболочку на ~150 строк кода

0 - базовые знания которые нам понадобятся

1-базовые знания языка програмирования C

Лучше всего надо будет прочитать эту книгу для овладения базовыми навыками программирования на C
Лучше всего надо будет прочитать эту книгу для овладения базовыми навыками программирования на 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. Запуск внешней программы, если внутренняя не работает

Что это значит? Если ни одна из команд cdpwdexithelp не сработала — значит, команда внешняя, и мы её должны запустить в отдельном процессе. Итак:

Сначала создаём копию текущего процесса, потом проверяем, что он создался правильно.

c

pid = fork();

if (pid < 0) {
    perror("fork");
    continue;
}

Если процесс создался правильно, то execvp ищет программу в директориях, указанных в переменной PATH. Тут стоит уточнить: поскольку это учебный проект, нацеленный для новичков, то у нас всего 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-запускать внешние программы : lsgrepcat и другие

Заключение

Спасибо за прочтение! Это был довольно интересный опыт для меня. Я надеюсь что вам понравилось.

Если у вас есть замечания по статье или по коду — пишите, наверняка есть более опытный и профессиональный программист на C, который может помочь как и читателям статьи, так и мне.