Shell-программирование: от новичка до уверенного уровня

Практический курс по написанию Bash-скриптов для автоматизации задач в Linux. Вы освоите синтаксис языка, научитесь управлять файловой системой Unix и взаимодействовать с системными вызовами.

Базовые конструкции Shell: переменные, условия и циклы

Базовые конструкции Shell: переменные, условия и циклы

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

Написание скриптов начинается с понимания того, как программа хранит данные, принимает решения и повторяет действия.

Переменные: память вашего скрипта

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

В Shell создание переменной происходит без указания ее типа. Главное правило синтаксиса — строгое отсутствие пробелов вокруг знака равенства.

USER_NAME="Anna"
FILES_COUNT=150

Чтобы получить значение переменной, перед ее именем ставится знак доллара: echo $USER_NAME.

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

TARGET_DIR=${1:-"/tmp/backup"}
echo "Резервное копирование в директорию: $TARGET_DIR"

Если первый аргумент скрипта ($1) пуст или не задан, переменная TARGET_DIR автоматически получит значение /tmp/backup.

Сохранение результатов системных вызовов

Скрипты часто выступают связующим звеном между различными системными утилитами Unix. Чтобы сохранить результат работы системного вызова или внешней программы в переменную, используется синтаксис $(команда).

Например, нам нужно создать архив, в названии которого будет зафиксирована текущая дата. Для этого мы вызываем системную утилиту date:

CURRENT_DATE=$(date +%Y-%m-%d)
ARCHIVE_NAME="backup_$CURRENT_DATE.tar.gz"
echo "Будет создан архив: $ARCHIVE_NAME"

Если скрипт запущен 25 октября 2023 года, переменная ARCHIVE_NAME получит значение backup_2023-10-25.tar.gz. Это базовый паттерн при работе с файловой системой, позволяющий избежать случайной перезаписи старых резервных копий.

Одинарные и двойные кавычки

При работе со строками критически важно различать типы кавычек. Они определяют, будет ли интерпретатор обрабатывать специальные символы внутри текста.

Тип кавычек Название Поведение интерпретатора Пример Результат
" " Двойные Раскрывает значения переменных и выполняет команды echo "Привет, $USER_NAME" Привет, Anna
' ' Одинарные Экранирует все символы, воспринимает текст буквально echo 'Привет, $USER_NAME' Привет, $USER_NAME
` ` Обратные Выполняет команду внутри и возвращает ее вывод echo "Файлов: \ls | wc -l`"` Файлов: 42

Программирование — это искусство разбивать сложную задачу на последовательность простых шагов, которые может выполнить машина.

Дональд Кнут

Условные конструкции: логика принятия решений

Скрипт редко выполняется линейно. Обычно ему нужно реагировать на внешние обстоятельства: проверять наличие файлов, права доступа или объем свободного места. За эту логику отвечают условные выражения.

Базовый синтаксис условия выглядит так:

if [ условие ]; then
    # команды, если условие истинно
elif [ другое_условие ]; then
    # команды, если второе условие истинно
else
    # команды, если все условия ложны
fi

Математические и строковые сравнения

В классической математике для сравнения чисел используются стандартные операторы: x>10x > 10, ageqba \\geq b, n<100n < 100, mleqkm \\leq k, aneqba \\neq b. В Shell внутри квадратных скобок [ ] применяются специальные буквенные флаги, так как символы больше и меньше зарезервированы для перенаправления потоков ввода-вывода.

  • x=yx = y записывается как -eq (equal)
  • aneqba \\neq b записывается как -ne (not equal)
  • x>10x > 10 записывается как -gt (greater than)
  • ageqba \\geq b записывается как -ge (greater or equal)
  • n<100n < 100 записывается как -lt (less than)
  • mleqkm \\leq k записывается как -le (less or equal)

Пример скрипта, который проверяет количество переданных аргументов. Если их меньше двух, программа завершается с ошибкой:

ARGS_COUNT=$#

if [ "$ARGS_COUNT" -lt 2 ]; then
    echo "Ошибка: требуется минимум 2 аргумента."
    exit 1
fi

Работа с файловой системой Unix

Одной из главных задач системного администратора является взаимодействие с файлами. Bash предоставляет встроенные операторы для проверки состояния файловой системы:

  • -f FILE — существует ли обычный файл.
  • -d DIR — существует ли директория.
  • -r FILE — доступен ли файл для чтения.
  • -x FILE — является ли файл исполняемым.

Допустим, нам нужно создать резервную копию базы данных, но только если директория для бэкапов существует. Если ее нет — скрипт должен ее создать.

BACKUP_DIR="/var/backups/db"

if [ ! -d "$BACKUP_DIR" ]; then
    echo "Директория не найдена. Создаем: $BACKUP_DIR"
    mkdir -p "$BACKUP_DIR"
else
    echo "Директория $BACKUP_DIR уже существует. Начинаем копирование."
fi

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

Комбинирование условий: логические И и ИЛИ

Часто требуется проверить сразу несколько параметров. Для этого применяются логические операторы:

  • && (Логическое И) — истинно, если оба условия верны.
  • || (Логическое ИЛИ) — истинно, если хотя бы одно условие верно.

Пример: скрипт должен удалить временный файл, но только если это действительно файл (не директория) и у нас есть права на его удаление (права на запись).

TEMP_FILE="/tmp/cache.bin"

if [ -f "$TEMP_FILE" ] && [ -w "$TEMP_FILE" ]; then
    rm "$TEMP_FILE"
    echo "Временный файл успешно удален."
fi

Циклы: автоматизация повторяющихся действий

Когда нужно выполнить одну и ту же операцию для множества объектов, на помощь приходят циклы. В Shell наиболее популярны два вида циклов: for и while.

Цикл for: перебор элементов

Цикл for идеально подходит для итерации по спискам, массивам или файлам в директории. Он берет каждый элемент из заданного набора и выполняет для него блок кода.

Синтаксис:

for ПЕРЕМЕННАЯ in СПИСОК; do
    # команды
done

Рассмотрим реальную задачу: у нас есть папка с изображениями в формате JPG, и нам нужно изменить их расширение на PNG.

for image in *.jpg; do
    new_name="${image%.jpg}.png"
    mv "$image" "$new_name"
    echo "Файл $image переименован в $new_name"
done

В этом примере *.jpg раскрывается в список всех файлов с таким расширением в текущей директории. Конструкция ${image%.jpg} — это встроенная функция Bash для удаления подстроки с конца переменной.

Также for умеет работать с числовыми диапазонами. Если нужно создать 5 тестовых пользователей:

for i in {1..5}; do
    echo "Создание пользователя test_user_$i"
    # Здесь мог бы быть системный вызов useradd
done

Цикл while: выполнение по условию

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

Частый сценарий использования while — построчное чтение текстового файла. Например, у нас есть файл servers.txt со списком IP-адресов, и мы хотим проверить доступность каждого из них:

while read -r ip_address; do
    if ping -c 1 -W 1 "$ip_address" > /dev/null; then
        echo "Сервер $ip_address доступен"
    else
        echo "Сервер $ip_address НЕ отвечает"
    fi
done < servers.txt

Здесь команда read считывает строку из файла и помещает ее в переменную ip_address. Цикл продолжается, пока в файле не закончатся строки. Перенаправление < servers.txt в конце цикла передает содержимое файла на стандартный ввод цикла.

Управление потоком: break и continue

Иногда требуется прервать выполнение цикла досрочно или пропустить текущую итерацию. Для этого существуют операторы break и continue.

  • break полностью останавливает цикл и передает управление следующей за ним команде.
  • continue прерывает только текущую итерацию и переходит к следующему элементу.

Пример использования break: поиск конкретного файла в архиве. Как только файл найден, продолжать перебор остальных файлов бессмысленно.

for file in /var/log/*.log; do
    if grep -q "CRITICAL ERROR" "$file"; then
        echo "Критическая ошибка найдена в файле: $file"
        break
    fi
done

В данном случае, если в директории 100 лог-файлов, и ошибка нашлась во втором, скрипт не будет проверять оставшиеся 98 файлов, сэкономив вычислительные ресурсы и время выполнения.

Итоги

  • Переменные в Shell создаются без указания типов и строго без пробелов вокруг знака равенства. Для безопасной подстановки значений по умолчанию используется синтаксис ${VAR:-default}.
  • Одинарные кавычки экранируют все символы, воспринимая текст буквально, а двойные позволяют интерпретатору раскрывать значения переменных и выполнять встроенные команды.
  • Условные конструкции if-then-else опираются на буквенные операторы (например, -gt для x>10x > 10) при сравнении чисел и на специальные флаги (-f, -d) при проверке объектов файловой системы Unix.
  • Циклы for эффективны для перебора известных списков и файлов, а while отлично справляется с чтением данных построчно и выполнением действий до изменения определенного условия.

Разработка скриптов для работы с Unix файловой системой

Разработка скриптов для работы с Unix файловой системой

Представьте, что на корпоративном сервере внезапно закончилось свободное место. База данных остановилась, пользователи не могут загрузить файлы, а бизнес теряет деньги каждую минуту. Вручную искать огромные временные логи среди миллионов других документов — задача безнадежная и долгая. Именно здесь раскрывается истинная сила автоматизации, когда базовые логические конструкции объединяются с мощью системных утилит Unix.

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

Поиск файлов: хирургическая точность

Как найти конкретный документ, если вы не помните его точного названия, но знаете, что он был создан на прошлой неделе и весит больше гигабайта? Для таких задач применяется системная утилита find — один из самых мощных инструментов в арсенале инженера.

Она не просто ищет файлы по имени, но и умеет фильтровать их по десяткам атрибутов: размеру, владельцу, правам доступа и времени модификации.

Рассмотрим скрипт, который предотвращает переполнение диска. Он находит все лог-файлы старше 30 дней и размером более 500 мегабайт, а затем удаляет их:

TARGET_DIR="/var/log"

if [ -d "$TARGET_DIR" ]; then
    find "$TARGET_DIR" -type f -name "*.log" -mtime +30 -size +500M -exec rm {} \;
    echo "Очистка старых логов завершена."
fi

В этом примере флаг -type f указывает, что мы ищем именно файлы (а не директории). Параметр -mtime +30 отбирает объекты, измененные более 30 дней назад, а -size +500M фильтрует их по объему. Конструкция -exec rm {} \; — это системный вызов, который применяет команду удаления к каждому найденному элементу. Если на сервере скопилось 40 таких файлов, скрипт мгновенно освободит 20 гигабайт дискового пространства.

Перенаправление потоков: управление реками данных

Куда исчезает текст, когда программа завершает работу? В архитектуре Unix любая программа имеет три стандартных потока данных:

  1. Стандартный ввод (stdin) — откуда программа получает данные.
  2. Стандартный вывод (stdout) — куда программа отправляет успешный результат.
  3. Стандартный вывод ошибок (stderr) — куда отправляются сообщения о сбоях.

По умолчанию вывод направляется на экран терминала. Однако в скриптах данные часто нужно сохранять для последующего анализа. Для этого используются операторы перенаправления.

Оператор Назначение Пример использования Результат
> Перезапись файла echo "Старт" > log.txt Файл создается заново, старые данные удаляются
>> Добавление в конец echo "Успех" >> log.txt Текст дописывается в конец существующего файла
2> Перенаправление ошибок ls /fake 2> error.log В файл попадет только текст системной ошибки
2>&1 Объединение потоков make test > out.txt 2>&1 Успешный вывод и ошибки запишутся в один файл

Иногда возникает потребность одновременно вывести данные на экран (чтобы видеть прогресс) и сохранить их в файл. Для этого применяется утилита tee:

echo "Обновление системы запущено" | tee -a /var/log/update.log
apt-get update | tee -a /var/log/update.log

Флаг -a (append) заставляет tee работать аналогично оператору >>, дописывая информацию в конец файла, не уничтожая предыдущие записи.

Пишите программы так, чтобы они делали что-то одно и делали это хорошо. Пишите программы так, чтобы они работали вместе.

Дуглас Макилрой

Конвейеры: сборка сложных механизмов

Символ вертикальной черты | называется конвейером (pipe). Он берет стандартный вывод одной программы и передает его на стандартный ввод другой. Это позволяет создавать сложные цепочки обработки данных без использования временных файлов.

Допустим, веб-сервер подвергся атаке, и нам нужно быстро вычислить IP-адрес, с которого поступает больше всего запросов. Мы можем написать скрипт, анализирующий файл access.log:

LOG_FILE="/var/log/nginx/access.log"

echo "Топ-3 самых активных IP-адресов:"
awk '{print $1}' "$LOG_FILE" | sort | uniq -c | sort -nr | head -n 3

Разберем этот механизм по шагам:

  1. Утилита awk извлекает первое слово из каждой строки лога (обычно это IP-адрес).
  2. Команда sort сортирует адреса по алфавиту, группируя одинаковые.
  3. Утилита uniq -c подсчитывает количество подряд идущих одинаковых строк.
  4. Второе применение sort -nr сортирует результат математически по убыванию (nn — числовая сортировка, rr — обратный порядок).
  5. Команда head -n 3 оставляет только три верхние строчки.

Если с одного адреса пришло 15 000 запросов, а с остальных по 10-20, этот скрипт выявит аномалию за доли секунды, позволив администратору оперативно заблокировать угрозу.

Управление правами доступа

Безопасность файловой системы опирается на строгую модель разграничения прав. Каждый файл имеет владельца, группу и набор разрешений: на чтение (read), запись (write) и выполнение (execute).

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

В Unix права часто записываются в восьмеричной системе счисления. Каждому действию присвоен числовой вес:

  • Чтение (rr) = 4
  • Запись (ww) = 2
  • Выполнение (xx) = 1

Чтобы получить итоговые права для пользователя, числа складываются. Например, права на чтение и запись дадут в сумме 6 (4+24 + 2). Права назначаются тремя цифрами: для владельца, для группы и для остальных.

Напишем скрипт, который перебирает все конфигурационные файлы в папке и устанавливает безопасные права (чтение и запись только для владельца — код 600):

CONFIG_DIR="/etc/myapp"

for file in "$CONFIG_DIR"/*.conf; do
    if [ -f "$file" ]; then
        chmod 600 "$file"
        chown root:root "$file"
        echo "Права доступа для $file обновлены."
    fi
done

Здесь цикл for перебирает файлы, условие if [ -f ... ] гарантирует, что мы работаем именно с файлами, а системные вызовы chmod и chown меняют разрешения и владельца объекта соответственно.

Итоги

  • Утилита find позволяет осуществлять глубокий поиск по файловой системе, фильтруя объекты по размеру, дате изменения и типу, а также выполнять над ними системные команды.
  • Операторы > и >> управляют перенаправлением потоков данных, позволяя сохранять результаты работы скриптов в файлы (с перезаписью или добавлением).
  • Конвейеры | объединяют простые утилиты (awk, sort, grep) в мощные механизмы для потоковой обработки текста и анализа логов.
  • Автоматизация работы с утилитами chmod и chown внутри циклов помогает поддерживать безопасность файловой системы, массово корректируя права доступа.

Использование системных вызовов и управление процессами

Использование системных вызовов и управление процессами

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

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

Анатомия процесса и системные вызовы

Когда вы вводите команду в терминале и нажимаете Enter, программа не начинает выполняться сама по себе. Оболочка Bash обращается к ядру операционной системы с просьбой выделить ресурсы.

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

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

Создание нового процесса в Unix опирается на два фундаментальных системных вызова:

  1. fork — создает точную копию текущего процесса (родителя). Новый процесс называется дочерним. Он получает копию памяти родителя, но имеет собственный уникальный идентификатор.
  2. exec — заменяет память дочернего процесса новой программой.

Программа — это заклинание, а процесс — это дух, которого вы вызвали этим заклинанием.

Эрик Рэймонд

Каждый процесс в системе получает уникальный числовой идентификатор — PIDPID (Process ID). Значение идентификатора всегда строго положительное, то есть PID>0PID > 0. Максимальное значение зависит от настроек ядра, но в классических системах PID≤32768PID \leq 32768. У каждого процесса также есть PPIDPPID (Parent Process ID) — идентификатор процесса, который его породил.

Мониторинг: поиск затерянных программ

Как найти процесс, который потребляет 100% ресурсов процессора и тормозит весь сервер? Для этого системные администраторы используют утилиты мониторинга.

Команда ps делает моментальный снимок текущих процессов. Чаще всего она используется с флагами aux:

ps aux | grep nginx

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

Процессы не всегда активно выполняются. Они постоянно меняют свои состояния в зависимости от системных событий.

Код состояния Название Описание
R Running Процесс выполняется в данный момент или ожидает очереди на процессоре.
S Sleeping Процесс спит, ожидая события (например, ввода от пользователя или ответа от диска).
D Uninterruptible Sleep Глубокий сон, обычно связанный с ожиданием ввода-вывода. Процесс нельзя убить.
Z Zombie Процесс завершился, но родитель еще не считал код его возврата. Он не потребляет память, но занимает PIDPID.
T Stopped Процесс приостановлен (например, комбинацией Ctrl+Z).

Для наблюдения за системой в реальном времени применяется утилита top (или ее улучшенная версия htop). Она выводит динамически обновляемый список процессов, отсортированный по потреблению ресурсов. Если сервер начал тормозить, запуск top — это первый шаг к диагностике проблемы.

Управление фоновыми задачами

Вернемся к проблеме долгого резервного копирования. Если запустить скрипт обычным образом, он привязывается к текущему терминалу. Закрытие терминала отправляет всем привязанным процессам сигнал об отключении, и они завершаются.

Чтобы освободить терминал для других команд, процесс можно запустить в фоне, добавив амперсанд & в конец команды:

./heavy_backup.sh &

Система вернет номер фоновой задачи и ее PIDPID, например: [1] 4592. Теперь вы можете вводить новые команды, пока бэкап создается в фоне.

Однако, если вы закроете SSH-сессию, процесс 4592 все равно будет уничтожен. Чтобы сделать процесс полностью независимым от терминала, используется системная утилита nohup (no hangup):

nohup ./heavy_backup.sh > backup.log 2>&1 &

В этом примере мы отвязали скрипт от терминала, запустили его в фоне и перенаправили весь вывод (включая ошибки) в файл backup.log. Даже если вы выключите свой рабочий компьютер, сервер продолжит выполнять скрипт. Если скрипт обрабатывает 500 гигабайт данных со скоростью 50 мегабайт в секунду, он будет безопасно работать в фоне около 170 минут.

Для управления фоновыми задачами в рамках одной сессии используются команды:

  • jobs — показывает список фоновых задач текущего терминала.
  • fg %1 (foreground) — возвращает задачу номер 1 на передний план.
  • bg %1 (background) — возобновляет выполнение приостановленной задачи в фоне.

Сигналы: язык общения с процессами

Как операционная система заставляет процесс остановиться? Она не выдергивает виртуальный шнур из розетки, а отправляет процессу сигнал. Сигналы — это простейшая форма межпроцессного взаимодействия (IPC).

Когда вы нажимаете Ctrl+C в терминале, оболочка отправляет активному процессу сигнал прерывания. Процесс получает его и, как правило, завершает свою работу.

Для ручной отправки сигналов используется системный вызов и одноименная утилита kill. Несмотря на пугающее название, она умеет отправлять любые сигналы, а не только убивать.

Самые важные сигналы, которые должен знать каждый инженер:

  • SIGINT (код 2) — прерывание с клавиатуры (Ctrl+C). Программа может перехватить его и корректно завершить работу.
  • SIGTERM (код 15) — мягкое требование завершения. Это сигнал по умолчанию для команды kill. Программа получает время на сохранение данных и закрытие файлов.
  • SIGKILL (код 9) — жесткое убийство. Этот сигнал обрабатывается не самой программой, а ядром ОС. Процесс уничтожается мгновенно, без возможности сохранить данные.

Если веб-сервер завис, правильный алгоритм действий выглядит так:

# Сначала просим процесс завершиться мягко
kill 4592

# Если через 10 секунд процесс все еще жив (состояние D или зависание),
# применяем крайнюю меру
kill -9 4592

Использование kill -9 должно быть исключительной мерой. Если применить его к базе данных во время записи транзакции, высок риск повредить файлы на диске.

Перехват сигналов внутри скрипта

Профессиональные скрипты умеют реагировать на сигналы. Представьте, что ваш скрипт скачивает временные файлы в директорию /tmp. Если пользователь нажмет Ctrl+C до завершения работы, временные файлы останутся лежать на диске мертвым грузом, постепенно съедая свободное место.

Чтобы этого избежать, в Bash существует встроенная команда trap. Она позволяет назначить функцию-обработчик, которая выполнится при получении определенного сигнала.

#!/bin/bash

TEMP_DIR="/tmp/my_script_cache"
mkdir -p "$TEMP_DIR"

# Назначаем обработчик сигналов SIGINT (2) и SIGTERM (15)
trap "echo 'Получен сигнал прерывания. Очистка...'; rm -rf $TEMP_DIR; exit 1" SIGINT SIGTERM

echo "Скрипт работает. Нажмите Ctrl+C для отмены."

# Имитация долгой работы
for i in {1..100}; do
    touch "$TEMP_DIR/file_$i.tmp"
    sleep 1
done

# Нормальное завершение
rm -rf "$TEMP_DIR"
echo "Работа успешно завершена."

В этом примере, если на 50-й секунде пользователь нажмет Ctrl+C, скрипт не просто оборвется. Ядро передаст сигнал интерпретатору Bash, сработает конструкция trap, скрипт выведет сообщение, удалит за собой мусор из /tmp и только после этого завершится с кодом ошибки 1. Это признак качественной и безопасной автоматизации.

Итоги

  • Создание процессов в Unix базируется на системных вызовах fork (копирование) и exec (замещение). Каждый процесс имеет уникальный идентификатор PIDPID.
  • Утилиты ps и top позволяют находить ресурсоемкие процессы и анализировать их состояния (от активного выполнения до "зомби").
  • Для запуска долгих скриптов, устойчивых к закрытию терминала, используется комбинация утилиты nohup и оператора фонового выполнения &.
  • Управление процессами осуществляется через сигналы. SIGTERM (15) просит программу завершиться корректно, а SIGKILL (9) принудительно уничтожает ее на уровне ядра.
  • Команда trap позволяет скриптам перехватывать сигналы прерывания, обеспечивая безопасное завершение работы и очистку временных файлов.