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

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

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

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

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

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

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

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

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

Чтобы получить значение переменной, перед ее именем ставится знак доллара: echo {1:-"/tmp/backup"} echo "Резервное копирование в директорию: 1) пуст или не задан, переменная TARGET_DIR автоматически получит значение /tmp/backup.

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

Скрипты часто выступают связующим звеном между различными системными утилитами Unix. Чтобы сохранить результат работы системного вызова или внешней программы в переменную, используется синтаксис (date +%Y-%m-%d) ARCHIVE_NAME="backup_ARCHIVE_NAME"

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

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

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

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

Синтаксис:

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

bash for i in {1..5}; do echo "Создание пользователя test_user_ip_address" > /dev/null; then echo "Сервер ip_address НЕ отвечает" fi done < servers.txt bash for file in /var/log/*.log; do if grep -q "CRITICAL ERROR" "file" break fi done `

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

Итоги

Переменные в Shell* создаются без указания типов и строго без пробелов вокруг знака равенства. Для безопасной подстановки значений по умолчанию используется синтаксис x > 10$) при сравнении чисел и на специальные флаги (-f, -d) при проверке объектов файловой системы Unix. * Циклы for эффективны для перебора известных списков и файлов, а while отлично справляется с чтением данных построчно и выполнением действий до изменения определенного условия.

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

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

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

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

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

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

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

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

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

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

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

  • Стандартный ввод (stdin) — откуда программа получает данные.
  • Стандартный вывод (stdout) — куда программа отправляет успешный результат.
  • Стандартный вывод ошибок (stderr) — куда отправляются сообщения о сбоях.
  • По умолчанию вывод направляется на экран терминала. Однако в скриптах данные часто нужно сохранять для последующего анализа. Для этого используются операторы перенаправления.

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

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

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

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

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

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

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

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

  • Утилита awk извлекает первое слово из каждой строки лога (обычно это IP-адрес).
  • Команда sort сортирует адреса по алфавиту, группируя одинаковые.
  • Утилита uniq -c подсчитывает количество подряд идущих одинаковых строк.
  • Второе применение sort -nr сортирует результат математически по убыванию ( — числовая сортировка, — обратный порядок).
  • Команда head -n 3 оставляет только три верхние строчки.
  • Если с одного адреса пришло 15 000 запросов, а с остальных по 10-20, этот скрипт выявит аномалию за доли секунды, позволив администратору оперативно заблокировать угрозу.

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

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

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

    В Unix права часто записываются в восьмеричной системе счисления. Каждому действию присвоен числовой вес: * Чтение () = 4 * Запись () = 2 * Выполнение () = 1

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

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

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

    Итоги

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    В этом примере мы отвязали скрипт от терминала, запустили его в фоне и перенаправили весь вывод (включая ошибки) в файл 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 -9 должно быть исключительной мерой. Если применить его к базе данных во время записи транзакции, высок риск повредить файлы на диске.

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

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

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

    ``bash #!/bin/bash

    TEMP_DIR="/tmp/my_script_cache" mkdir -p "TEMP_DIR; exit 1" SIGINT SIGTERM

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

    Имитация долгой работы

    for i in {1..100}; do touch "i.tmp" sleep 1 done

    Нормальное завершение

    rm -rf "PID$. * Утилиты
    ps и top позволяют находить ресурсоемкие процессы и анализировать их состояния (от активного выполнения до "зомби"). * Для запуска долгих скриптов, устойчивых к закрытию терминала, используется комбинация утилиты nohup и оператора фонового выполнения &. * Управление процессами осуществляется через сигналы. SIGTERM (15) просит программу завершиться корректно, а SIGKILL (9) принудительно уничтожает ее на уровне ядра. * Команда trap` позволяет скриптам перехватывать сигналы прерывания, обеспечивая безопасное завершение работы и очистку временных файлов.