Блок 15

Bash-скрипты и автоматизация

Linux: блок 15

> Что вас ждёт в этом блоке: до сих пор вы вводили команды одну за другой, вручную. Теперь вы научитесь записывать последовательности команд в файлы — скрипты — и заставлять компьютер выполнять сло…

Обновлено: · DeeNet

Введение в блок

> Что вас ждёт в этом блоке: до сих пор вы вводили команды одну за другой, вручную. Теперь вы научитесь записывать последовательности команд в файлы — скрипты — и заставлять компьютер выполнять сложную логику самостоятельно: принимать решения (if), повторять действия (циклы), обрабатывать переданные параметры, реагировать на ошибки и запускаться по расписанию без вашего участия. Это центральный навык, который превращает пользователя Linux в администратора, способного автоматизировать рутину.

>

> Что нужно знать перед началом: весь предыдущий материал курса — скрипты это, по сути, объединение всех уже изученных команд (Блоки 1–14) в единую программу. Особенно важны: работа с файлами (Блок 5), права доступа (Блок 8), grep/sed/потоки и конвейеры (Блок 6), а также материал по безопасности (Блок 14), который мы будем активно автоматизировать.

Урок 15.1. Что такое скрипт и зачем он нужен

Понять, что такое shell-скрипт, чем он отличается от простого набора команд в терминале, где применяются скрипты на практике, и как устроена концепция «интерпретируемого» выполнения кода.

Теория

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

Скрипт (script) — это текстовый файл, содержащий последовательность команд, которые интерпретатор командной строки (shell) выполняет одну за другой, как если бы вы вводили их вручную в терминале. Вместо того чтобы печатать команды по одной, вы один раз пишете их все в файл, а затем запускаете этот файл — и все команды выполняются автоматически, в заданном порядке.

Bash-скрипт — это скрипт, написанный на языке командной оболочки Bash (Bourne Again Shell), той самой оболочки, в которой мы работаем весь курс (мы упоминали её в Блоке 1 и уроке 7.1 в контексте редакторов и командной строки). Технически Bash — это не просто интерпретатор отдельных команд, а полноценный, хоть и специфический, язык программирования: в нём есть переменные, условия, циклы, функции — всё то, что мы будем изучать в этом блоке.

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

Bash-скрипт — интерпретируемый: когда вы запускаете скрипт, программа /bin/bash (тот самый интерпретатор, чей путь мы уже видели в файле /etc/passwd в Блоке 8, когда изучали оболочки пользователей) читает файл построчно и выполняет каждую команду так, как если бы вы вводили её в терминал вручную.

Где на практике применяются bash-скрипты:

Философия Unix и роль скриптов. Мы уже несколько раз в курсе (начиная с Блока 6, при изучении конвейеров) касались философии Unix: «каждая программа должна хорошо делать одну вещь». Bash-скрипты — это именно тот механизм, который позволяет комбинировать множество маленьких специализированных программ (grep, awk, sed, find и десятки других, которые мы изучали) в сложные автоматизированные процессы. Скрипт — это не замена изученным командам, а способ организовать их совместную работу.

Практика

В этом вводном уроке мы не будем ещё писать полноценные скрипты (это тема следующего урока), а посмотрим на уже существующие скрипты в системе, чтобы увидеть, как это выглядит на практике, и оценим разницу между ручным вводом команд и их автоматизацией.

Шаг 1. Найдите и изучите содержимое системного скрипта — посмотрите на скрипты, которые система автоматически запускает раз в день (подробнее про cron — в уроке 15.9 этого же блока):

```bash

ls -la /etc/cron.daily/

```

Что вы увидите: список файлов-скриптов.

[Скриншот терминала]

Шаг 2. Посмотрите содержимое одного из таких скриптов (не изменяйте его — только просмотр):

```bash

cat /etc/cron.daily/apt-compat 2>/dev/null || ls /etc/cron.daily/

```

Что вы увидите: реальный, работающий bash-скрипт, начинающийся со специальной первой строки (#!/bin/sh или похожей) — эту конструкцию мы подробно разберём в следующем уроке.

Шаг 3. Вспомните свой собственный скрипт из урока 14.6 (~/security_check.sh) — это уже был полноценный, хоть и простой, bash-скрипт. Откройте его снова и посмотрите на структуру:

```bash

cat ~/security_check.sh

```

Шаг 4. Сравните два подхода к одной и той же задаче — «вывести дату, имя текущего пользователя и объём свободного места на диске».

Способ 1 — вручную, три отдельные команды:

```bash

date

whoami

df -h /

```

Способ 2 — представим, что это уже одна команда (в следующем уроке мы превратим это в реальный скрипт):

```bash

echo "Дата: $(date), Пользователь: $(whoami), Место на диске:" && df -h /

```

Обратите внимание на использование $(...) — эту конструкцию подстановки команд (command substitution) мы уже видели мельком в предыдущих блоках, и будем использовать её очень активно в скриптах: она позволяет вставить результат выполнения одной команды прямо в текст или аргументы другой.

Шаг 5. Посмотрите, какие интерпретаторы скриптов установлены в вашей системе, изучив файл /etc/shells (мы кратко касались его в Блоке 8):

```bash

cat /etc/shells

```

Что вы увидите: список доступных командных оболочек, включая /bin/bash — тот интерпретатор, который мы будем использовать для всех скриптов этого блока.

Разбор команд

Конструкция $(команда) — подстановка команд

Возможные ошибки

Почему возникает: обратные кавычки — более старый синтаксис, встречающийся в старых скриптах и обучающих материалах.

Как определить: синтаксис технически работает, но становится трудночитаемым при попытке вложить одну подстановку в другую.

Как исправить/избежать: всегда использовать $(команда) в новом коде — это современный стандарт, поддерживающий вложенность: $(команда1 $(команда2)) читается и работает корректно, тогда как с обратными кавычками это крайне запутанно.

Практические задания

  1. Найдите в системе ещё один пример реального bash-скрипта (подсказка: посмотрите файлы в /usr/bin/, у многих утилит с помощью file можно определить, что это именно shell-скрипт, а не бинарный исполняемый файл — например, переберите несколько файлов командой file /usr/bin/* | grep "shell script" | head -5). Просмотрите его содержимое и запишите в конспект, узнаёте ли вы какие-либо из использованных в нём команд.
  2. Объясните в конспекте своими словами, в чём разница между компилируемым и интерпретируемым языком, и к какой категории относится Bash.
  3. Придумайте (и опишите текстом, не обязательно писать код) три задачи из вашей повседневной работы с компьютером, которые вы могли бы автоматизировать с помощью bash-скрипта, если бы уже умели их писать.

Проверка знаний

Вопрос 1. Чем принципиально отличается выполнение команд «вручную одну за другой» от выполнения того же набора команд через bash-скрипт?

Ответ: Функционально результат тот же — команды выполняются в том же порядке. Разница в том, что скрипт: (1) гарантирует точное повторение последовательности без риска забыть шаг или ошибиться при ручном вводе, (2) можно запускать многократно одной командой, (3) можно передать другому человеку или запланировать на автоматический запуск (см. урок 15.9), (4) позволяет добавить логику (условия, циклы), которую невозможно реализовать простым последовательным вводом команд в терминал.

Вопрос 2. Что делает конструкция $(команда) в bash, и чем она отличается просто от вызова команды напрямую?

Ответ: $(команда) выполняет указанную команду и подставляет её вывод (то, что она печатает на экран) на своё место в тексте, как часть другой команды или строки. Это отличается от простого вызова команды тем, что результат используется не сам по себе, а становится входными данными для чего-то другого — например, аргументом другой команды или частью выводимого текста через echo.

Итоги урока

Вы изучили: определение скрипта, разницу между компилируемыми и интерпретируемыми языками, роль bash-скриптов в философии Unix и практике администрирования, конструкцию подстановки команд $(...).

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

В следующем уроке потребуется: понимание того, что скрипт — это текстовый файл с командами — теперь мы научимся правильно создавать, оформлять и запускать такой файл.

Урок 15.2. Shebang и первый скрипт

Написать, сделать исполняемым и запустить свой первый настоящий bash-скрипт, понять назначение специальной первой строки («shebang»), освоить разные способы запуска скриптов.

Теория

Shebang (она же «shabang», «hashbang», в русскоязычной среде иногда «шебанг») — это специальная первая строка скрипта, начинающаяся с символов #!, за которыми следует путь к интерпретатору, который должен выполнить этот файл:

```bash

#!/bin/bash

```

Зачем нужна эта строка. Когда система пытается выполнить файл как программу (например, вы запустили его командой ./script.sh), она смотрит на первые байты файла. Если они начинаются с #!, система интерпретирует всё, что после этого символа до конца строки, как путь к программе, которая должна выполнить остальное содержимое файла. Это позволяет ядру Linux (а точнее, механизму exec, который мы упоминали в Блоке 11 в контексте процессов) понять: «этот файл нужно не выполнять как двоичную программу напрямую, а передать в качестве аргумента вон той программе-интерпретатору».

Без shebang система не будет знать, каким интерпретатором выполнять файл, если вы запускаете его напрямую (./script.sh), и, скорее всего, попытается интерпретировать его как исполняемый бинарный файл — и выдаст ошибку.

Почему именно #!/bin/bash, а не что-то другое. Символ # в Bash обычно означает начало комментария (мы уже видели это в конфигурационных файлах ранее в курсе, например в ~/.ssh/config в Блоке 13) — то есть строка, начинающаяся с #, при обычном выполнении интерпретатором просто игнорируется. Специальная комбинация #! в самом начале файла (и только в самом начале — если это первая строка) — исключение из этого правила: она распознаётся не самим Bash, а ядром Linux, ещё до того, как файл будет передан какому-либо интерпретатору.

Альтернативные варианты shebang, которые вы можете встретить:

В этом курсе мы будем последовательно использовать #!/bin/bash как самый явный и предсказуемый вариант для Ubuntu.

Права на выполнение. Как мы подробно изучали в Блоке 8 (права доступа), файл должен иметь право на выполнение (execute, x), чтобы его можно было запустить напрямую как программу. Просто текстовый файл с командами внутри, даже с правильным shebang, не будет запускаться через ./script.sh, пока вы не дадите ему право на выполнение командой chmod +x.

Способы запуска скрипта — три варианта:

  1. ./script.sh — прямой запуск исполняемого файла. Требует: (а) права на выполнение (chmod +x), (б) явного указания пути (./ означает «в текущей директории» — мы разбирали это ещё в Блоке 5, объясняя, почему просто script.sh не сработает, если текущая директория не в PATH).
  1. bash script.sh — явный запуск через интерпретатор. В этом случае право на выполнение самого файла не обязательно — вы явно указываете, что bash должен прочитать и выполнить этот файл как аргумент, независимо от прав доступа к самому файлу на исполнение (важно: чтение файла всё равно требуется).
  1. source script.sh или его сокращённая форма . script.sh (точка с пробелом) — выполнить скрипт в текущей оболочке, а не в новой дочерней. Это принципиально иной механизм, который мы разберём подробнее ниже.

Важное отличие: новый процесс vs. текущая оболочка. Когда вы запускаете скрипт способом 1 или 2 (./script.sh или bash script.sh), создаётся новый дочерний процесс (вспомните иерархию процессов из Блока 11) — новая копия bash, которая выполняет команды скрипта, а после завершения скрипта этот дочерний процесс закрывается, и вы возвращаетесь в исходную оболочку. Любые изменения, которые скрипт делает — например, смена текущей директории командой cd внутри скрипта, — не влияют на вашу исходную оболочку, потому что происходят в отдельном, дочернем процессе.

source (или .) работает иначе: он выполняет команды скрипта прямо в текущей оболочке, без создания нового процесса. Поэтому если скрипт меняет текущую директорию или устанавливает переменные окружения, эти изменения сохранятся и после завершения скрипта, в вашей текущей сессии терминала. Именно поэтому source часто используется для загрузки настроек — например, если вы изменили файл ~/.bashrc (который мы, возможно, кратко упоминали ранее — это файл конфигурации, автоматически загружаемый при старте интерактивной оболочки), вы можете применить изменения командой source ~/.bashrc, не открывая новый терминал.

Практика

Шаг 1. Создайте директорию для скриптов, которую будем использовать на протяжении всего блока:

```bash

mkdir -p ~/scripts

cd ~/scripts

```

Шаг 2. Создайте файл первого скрипта с помощью nano (Блок 7):

```bash

nano hello.sh

```

Шаг 3. Введите следующее содержимое:

```bash

#!/bin/bash

echo "Привет! Меня зовут $(whoami)."

echo "Сегодня $(date +%A), $(date +%d.%m.%Y)."

echo "Я нахожусь в директории: $(pwd)"

echo "Скрипт успешно завершён."

```

Сохраните: Ctrl+O, Enter, Ctrl+X.

[Скриншот содержимого скрипта в nano]

Шаг 4. Попробуйте запустить скрипт напрямую, не давая ему прав на выполнение — посмотрим на ошибку:

```bash

./hello.sh

```

Что вы увидите: bash: ./hello.sh: Permission denied — файл существует и читается, но у него нет права на выполнение.

Шаг 5. Проверьте текущие права на файл (вспомните ls -l из Блока 8):

```bash

ls -l hello.sh

```

Что вы увидите: права вида -rw-r--r-- — нет x (execute) ни у кого.

Шаг 6. Дайте файлу право на выполнение для владельца:

```bash

chmod u+x hello.sh

ls -l hello.sh

```

Что вы увидите: теперь права -rwxr--r-- — у владельца появилось право x.

[Скриншот терминала с изменёнными правами]

Шаг 7. Запустите скрипт снова:

```bash

./hello.sh

```

Что вы увидите: вывод скрипта — приветствие, дату, текущую директорию.

[Скриншот успешного выполнения скрипта]

Шаг 8. Попробуйте второй способ запуска — через явный вызов интерпретатора (сработает, даже если убрать право на выполнение):

```bash

chmod u-x hello.sh

bash hello.sh

```

Что вы увидите: скрипт выполняется точно так же, несмотря на отсутствие права x — потому что мы явно указали интерпретатору прочитать и выполнить этот файл.

Шаг 9. Верните право на выполнение (стандартная практика для скриптов, которые предполагается запускать напрямую):

```bash

chmod u+x hello.sh

```

Шаг 10. Продемонстрируйте разницу между обычным запуском и source — создайте скрипт, который меняет директорию:

```bash

nano change_dir.sh

```

Содержимое:

```bash

#!/bin/bash

echo "До смены директории: $(pwd)"

cd /tmp

echo "После смены директории (внутри скрипта): $(pwd)"

```

Сохраните, дайте права на выполнение:

```bash

chmod +x change_dir.sh

```

Шаг 11. Запустите обычным способом и проверьте свою текущую директорию после завершения:

```bash

cd ~/scripts

./change_dir.sh

pwd

```

Что вы увидите: скрипт покажет, что внутри него директория сменилась на /tmp, но после завершения скрипта команда pwd в вашем терминале покажет ~/scripts — изменение не «просочилось» в вашу текущую сессию, потому что скрипт выполнялся в отдельном дочернем процессе.

[Скриншот терминала, демонстрирующий это поведение]

Шаг 12. Теперь запустите тот же скрипт через source и снова проверьте:

```bash

cd ~/scripts

source change_dir.sh

pwd

```

Что вы увидите: на этот раз после завершения скрипта ваша текущая директория действительно изменилась на /tmp — потому что source выполнил скрипт в текущей оболочке, а не в дочернем процессе.

[Скриншот терминала, демонстрирующий разницу]

Шаг 13. Вернитесь в директорию скриптов:

```bash

cd ~/scripts

```

Разбор команд

Shebang #!/bin/bash

Команда chmod +x файл

Команда source (и её сокращение .)

Возможные ошибки

Почему возникает: у файла нет права на выполнение.

Как исправить: chmod +x script.sh.

Почему возникает: это частая и обманчивая ошибка — на самом деле файл существует, но проблема в интерпретаторе, указанном в shebang: если файл был создан на Windows и содержит специфичные для Windows окончания строк (CRLF вместо привычного для Unix LF — мы упоминали проблему таких «невидимых» символов ранее в курсе), система не может найти команду /bin/bash\r (с невидимым символом возврата каретки в конце строки) — путь буквально не существует в такой форме.

Как определить: команда file script.sh покажет with CRLF line terminators для файлов с этой проблемой; также можно использовать cat -A script.sh (мы упоминали -A для отображения непечатаемых символов ранее) — в конце каждой строки будет виден символ ^M.

Как исправить: конвертировать файл в формат Unix: dos2unix script.sh (может потребоваться установка: sudo apt install dos2unix) или через sed: sed -i 's/\r$//' script.sh (мы разбирали sed в Блоке 6).

Как избежать: всегда создавать и редактировать скрипты непосредственно в Linux-редакторах (nano, vim), а если скрипт создан на другой системе — сразу проверять и конвертировать формат окончаний строк.

Почему возникает: невнимательность при создании файла.

Как определить: при запуске ./script.sh система может попытаться интерпретировать первую строку скрипта как обычную команду вашей текущей оболочки, что может привести к неожиданным ошибкам, если содержимое скрипта не является валидной единственной командой.

Как исправить: добавить #!/bin/bash как самую первую строку файла.

Практические задания

  1. Создайте скрипт system_info.sh, который выводит: имя хоста (hostname), версию ядра (uname -r), время работы системы (uptime) и количество вошедших пользователей (who | wc -l — используем wc из Блока 6). Сделайте его исполняемым и запустите.
  2. Намеренно создайте файл с CRLF-окончаниями строк (можно через printf 'echo hello\r\n' > crlf_test.sh — команда printf работает похоже на echo, но с более точным контролем формата вывода), попробуйте его запустить, изучите ошибку через cat -A, и исправьте с помощью sed из разбора ошибок выше.
  3. Продемонстрируйте на практике разницу source и обычного запуска ещё раз, но уже с переменной окружения вместо смены директории: создайте скрипт, который выполняет export MY_TEST_VAR="привет" (про export подробнее в следующем уроке про переменные), запустите его обычным способом и проверьте echo $MY_TEST_VAR (будет пусто), затем запустите через source и проверьте снова (переменная появится).

Проверка знаний

Вопрос 1. Зачем нужна строка #!/bin/bash в начале скрипта, и что произойдёт, если её не будет, при запуске скрипта способом bash script.sh?

Ответ: Shebang указывает системе, каким интерпретатором выполнять файл при прямом запуске (./script.sh) — без неё система не знает, как интерпретировать содержимое файла как программу. Однако если вы запускаете скрипт явно через bash script.sh, вы уже сами указали интерпретатор в команде — в этом случае shebang технически не требуется (хотя всё равно является хорошей практикой для ясности и на случай прямого запуска в будущем).

Вопрос 2. В чём разница между запуском скрипта командой ./script.sh и командой source script.sh, и когда предпочтительнее использовать второй вариант?

Ответ: ./script.sh запускает скрипт в новом дочернем процессе — любые изменения окружения (текущая директория, переменные) действуют только внутри этого дочернего процесса и исчезают после его завершения. source script.sh выполняет команды скрипта в текущей оболочке — изменения сохраняются и после завершения. source предпочтителен, когда цель скрипта — изменить состояние текущей сессии терминала: установить переменные окружения, сменить директорию, перезагрузить конфигурационный файл оболочки.

Итоги урока

Вы изучили: назначение shebang и его варианты, разницу между запуском через ./script.sh, bash script.sh и source script.sh, типичную проблему с CRLF-окончаниями строк.

Вы умеете: создавать, делать исполняемыми и запускать bash-скрипты разными способами, диагностировать и исправлять проблему CRLF.

В следующем уроке потребуется: умение писать и запускать скрипты — теперь мы сделаем их динамичными с помощью переменных и параметров командной строки.

Урок 15.3. Переменные, аргументы командной строки: `$1`, `$@`, `$#`

Освоить объявление и использование переменных в bash, научиться работать с параметрами, переданными скрипту при запуске, понять специальные переменные $1, $@, $# и другие.

Теория

Переменные в Bash. Переменная — это именованная область памяти, хранящая значение, к которому можно обращаться по имени. Мы уже неявно использовали переменные окружения (например, $HOME — путь к домашней папке, $PATH — список директорий для поиска команд, который упоминался в Блоке 5) — теперь научимся создавать собственные.

Объявление переменной:

```bash

имя_переменной=значение

```

Критически важное правило: никаких пробелов вокруг знака =. В отличие от многих языков программирования, в Bash имя = значение (с пробелами) — это ошибка (Bash интерпретирует это как попытку выполнить команду с именем имя, передав ей аргументы = и значение). Правильно строго без пробелов: имя=значение.

Обращение к значению переменной — через символ $ перед именем:

```bash

приветствие="Привет, мир!"

echo $приветствие

```

Фигурные скобки ${имя} — более явный и безопасный способ обращения к переменной, особенно когда имя переменной может «слипнуться» с окружающим текстом:

```bash

файл="отчёт"

echo "${файл}_2026.txt"

```

Без фигурных скобок (echo "$файл_2026.txt") Bash попытался бы найти переменную с именем файл_2026, а не файл — что привело бы к пустому результату. Хорошая практика — почти всегда использовать ${имя} вместо просто $имя в реальных скриптах.

Типы переменных в Bash — важная оговорка. В отличие от многих языков программирования, Bash не имеет строгой типизации: все переменные по умолчанию хранятся как строки текста, даже если выглядят как числа. Это важно понимать при попытках выполнять арифметику — простое сложение $a + $b не работает как ожидается без специальных конструкций (мы коснёмся арифметики в контексте условий и циклов далее в блоке).

Переменные окружения vs. локальные переменные скрипта. Переменные, объявленные простым имя=значение внутри скрипта, доступны только в текущем скрипте (и его дочерним процессам, если явно не экспортированы). Команда export:

```bash

export ИМЯ=значение

```

делает переменную частью окружения (environment) — то есть доступной не только текущему скрипту, но и любым программам/скриптам, которые этот скрипт в дальнейшем запускает (дочерним процессам). Мы упоминали эту разницу мельком в предыдущем уроке, разбирая эффект source.

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

```bash

./myscript.sh привет мир 123

```

эти слова становятся доступны внутри скрипта через специальные, автоматически создаваемые переменные:

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

```bash

if [ $# -eq 0 ]; then

echo "Ошибка: нужно передать хотя бы один аргумент"

exit 1

fi

```

(Подробно синтаксис if мы изучим в следующем уроке — здесь просто демонстрируем практическое применение $#.)

Значения по умолчанию для переменных — полезная конструкция ${переменная:-значение_по_умолчанию}:

```bash

имя="${1:-Гость}"

echo "Привет, $имя!"

```

Если $1 не был передан (пусто), переменной имя присваивается "Гость"; если $1 был передан — используется его значение.

Практика

Шаг 1. Создайте скрипт для практики с базовыми переменными:

```bash

cd ~/scripts

nano variables.sh

```

Содержимое:

```bash

#!/bin/bash

имя="Анна"

возраст=25

город="Рига"

echo "Имя: $имя"

echo "Возраст: $возраст"

echo "Город: ${город}"

файл="отчёт"

echo "Неправильно: $файл_2026.txt"

echo "Правильно: ${файл}_2026.txt"

текущая_дата=$(date +%Y-%m-%d)

echo "Сегодняшняя дата: $текущая_дата"

```

Сохраните, сделайте исполняемым и запустите:

```bash

chmod +x variables.sh

./variables.sh

```

Что вы увидите: значения переменных, и наглядную демонстрацию разницы между $файл_2026.txt (не найдёт нужную переменную, выведет пусто с окончанием _2026.txt) и ${файл}_2026.txt (сработает корректно).

[Скриншот терминала с результатом]

Шаг 2. Создайте скрипт для работы с аргументами командной строки:

```bash

nano args_demo.sh

```

Содержимое:

```bash

#!/bin/bash

echo "Имя скрипта: $0"

echo "Первый аргумент: $1"

echo "Второй аргумент: $2"

echo "Всего аргументов: $#"

echo "Все аргументы (\$@): $@"

echo "Все аргументы (\$): $"

echo "PID этого скрипта: $$"

```

Сохраните, сделайте исполняемым.

Шаг 3. Запустите с разными аргументами:

```bash

chmod +x args_demo.sh

./args_demo.sh

```

Что вы увидите: пустые значения для $1, $2, $# равен 0 — аргументы не переданы.

```bash

./args_demo.sh яблоко банан

```

Что вы увидите: $1 = яблоко, $2 = банан, $# = 2.

[Скриншот терминала]

```bash

./args_demo.sh один два три четыре пять

```

Что вы увидите: $# = 5, а $@/$* покажут все пять аргументов.

Шаг 4. Продемонстрируйте разницу между "$@" и "$*" при аргументах, содержащих пробелы — сначала создайте скрипт:

```bash

nano at_vs_star.sh

```

Содержимое:

```bash

#!/bin/bash

echo "--- Перебор через \"\$@\" ---"

for arg in "$@"; do

echo "Аргумент: [$arg]"

done

echo "--- Перебор через \"\$*\" ---"

for arg in "$*"; do

echo "Аргумент: [$arg]"

done

```

(Мы используем цикл for, который подробно разберём в уроке 15.5 — здесь достаточно интуитивно понять результат.)

```bash

chmod +x at_vs_star.sh

./at_vs_star.sh "первый аргумент" "второй аргумент"

```

Что вы увидите: при использовании "$@" каждый переданный аргумент (даже содержащий пробелы внутри кавычек) обрабатывается как отдельный элемент — цикл выполнится 2 раза. При "$*" все аргументы склеятся в одну строку — цикл выполнится только 1 раз с этой единой строкой.

[Скриншот терминала, демонстрирующий разницу]

Шаг 5. Создайте скрипт со значением по умолчанию и проверкой наличия аргументов:

```bash

nano greet.sh

```

Содержимое:

```bash

#!/bin/bash

имя="${1:-Гость}"

echo "Здравствуйте, $имя!"

if [ $# -eq 0 ]; then

echo "(Подсказка: вы можете передать своё имя как аргумент: ./greet.sh ВашеИмя)"

fi

```

```bash

chmod +x greet.sh

./greet.sh

./greet.sh Мария

```

Что вы увидите: в первом случае — приветствие «Гость» и подсказка; во втором — персонализированное приветствие без подсказки.

[Скриншот терминала]

Шаг 6. Изучите переменные окружения, доступные вашей текущей оболочке (уже существующие в системе, не созданные вами):

```bash

echo "Домашняя папка: $HOME"

echo "Текущий пользователь: $USER"

echo "Оболочка по умолчанию: $SHELL"

echo "Путь поиска команд: $PATH"

```

Шаг 7. Продемонстрируйте export — создайте два скрипта, чтобы увидеть разницу между обычной и экспортированной переменной:

```bash

nano set_var.sh

```

Содержимое:

```bash

#!/bin/bash

локальная_переменная="я локальная"

export ЭКСПОРТИРОВАННАЯ_ПЕРЕМЕННАЯ="я экспортирована"

echo "Внутри set_var.sh всё видно нормально."

./check_var.sh

```

```bash

nano check_var.sh

```

Содержимое:

```bash

#!/bin/bash

echo "Проверка из дочернего скрипта:"

echo "локальная_переменная = '$локальная_переменная'"

echo "ЭКСПОРТИРОВАННАЯ_ПЕРЕМЕННАЯ = '$ЭКСПОРТИРОВАННАЯ_ПЕРЕМЕННАЯ'"

```

```bash

chmod +x set_var.sh check_var.sh

./set_var.sh

```

Что вы увидите: локальная_переменная окажется пустой внутри дочернего скрипта check_var.sh (не была экспортирована), а ЭКСПОРТИРОВАННАЯ_ПЕРЕМЕННАЯ — будет видна и корректно выведена, потому что была экспортирована через export.

[Скриншот терминала, демонстрирующий разницу]

Разбор команд

Специальные переменные — сводная таблица:

| Переменная | Значение |

|---|---|

| $0 | Имя (путь) самого скрипта |

| $1...$9 | Аргументы 1–9 |

| ${10}, ${11}... | Аргументы от 10 и далее (обязательны фигурные скобки) |

| $# | Количество переданных аргументов |

| $@ | Все аргументы как отдельные слова |

| $* | Все аргументы как одна склеенная строка |

| $$ | PID текущего скрипта |

| $? | Код возврата последней выполненной команды |

Конструкция ${переменная:-значение}

Команда export

Возможные ошибки

Почему возникает: имя = значение (с пробелами) интерпретируется Bash как попытка выполнить команду с именем имя.

Как определить: сообщение вида имя: command not found.

Как исправить/избежать: всегда писать без пробелов: имя=значение.

Почему возникает: типичная причина — «слипание» имени переменной с последующим текстом без фигурных скобок ($переменнаятекст вместо ${переменная}текст), либо переменная была объявлена в одном скрипте, а используется в другом без export.

Как исправить: использовать ${переменная} вместо $переменная при наличии текста сразу после имени; использовать export, если переменная должна быть видна в дочерних процессах.

Практические задания

  1. Напишите скрипт calc_info.sh, принимающий два числа как аргументы ($1 и $2), и выводящий сообщение вида «Вы передали числа: 5 и 10. Всего аргументов: 2.» — используя $1, $2 и $#.
  2. Модифицируйте скрипт из задания 1 так, чтобы он проверял (if [ $# -lt 2 ], «меньше двух» — предвосхищая синтаксис следующего урока, но можно найти пример по аналогии с практикой этого урока) и выводил сообщение об ошибке, если передано меньше двух аргументов.
  3. Создайте скрипт, который использует $$ для создания временного файла с уникальным именем (например, /tmp/mydata_$$.tmp), записывает туда какой-то текст, выводит имя файла, а затем удаляет его. Объясните в конспекте, зачем может быть полезно использовать PID в имени временного файла (подсказка: подумайте о ситуации, когда скрипт запускается одновременно несколько раз).

Проверка знаний

Вопрос 1. В чём разница между $@ и $ при использовании внутри двойных кавычек ("$@" против "$"), особенно если один из переданных аргументов содержит пробел внутри себя?

Ответ: "$@" разворачивается в отдельные слова, сохраняя каждый переданный аргумент как самостоятельный элемент — даже если внутри аргумента есть пробелы (потому что он был передан в кавычках при вызове скрипта). "$" разворачивается в одну строку, где все аргументы склеены через пробел, независимо от того, где были границы исходных аргументов. При переборе аргументов в цикле это критически важное отличие: "$@" даёт по одному аргументу за итерацию, "$" даёт всё одной строкой за одну итерацию.

Вопрос 2. Почему export необходим, если вы хотите, чтобы переменная, установленная в одном скрипте, была видна в другом скрипте, который первый скрипт запускает?

Ответ: По умолчанию переменные, объявленные в скрипте (имя=значение), являются локальными для этого скрипта и не передаются автоматически дочерним процессам (например, другому скрипту, запущенному изнутри первого). export добавляет переменную в окружение (environment) процесса — а окружение, в отличие от обычных локальных переменных, действительно наследуется дочерними процессами при их создании.

Итоги урока

Вы изучили: объявление и использование переменных, правило отсутствия пробелов вокруг =, фигурные скобки ${}, специальные переменные аргументов ($0, $1...$9, $#, $@, $*, $$, $?), команду export, значения по умолчанию ${переменная:-значение}.

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

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

Урок 15.4. Условия: `if`, `test`, `[[ ]]`

Освоить условные конструкции bash: оператор if/elif/else, команду test и её эквивалент [ ], современный синтаксис [[ ]], а также основные операторы сравнения чисел, строк и проверки файлов.

Теория

Зачем нужны условия. До сих пор наши скрипты выполняли команды строго последовательно, одну за другой, без «развилок». Реальная автоматизация почти всегда требует принятия решений: «если места на диске мало — удалить старые логи, иначе просто вывести отчёт», «если файл существует — обработать его, иначе вывести ошибку». Именно это позволяет делать конструкция if.

Базовый синтаксис if:

```bash

if условие

then

команды, если условие истинно

fi

```

Чаще пишут в более компактной форме, размещая then на той же строке через точку с запятой:

```bash

if условие; then

команды

fi

```

Обязательное завершение блока if — ключевое слово fi («if» наоборот — характерная для Bash традиция обозначения конца блока).

Расширенная форма с альтернативами — elif и else:

```bash

if условие1; then

команды, если условие1 истинно

elif условие2; then

команды, если условие1 ложно, а условие2 истинно

else

команды, если ни одно условие не истинно

fi

```

Что именно является «условием» в Bash — важнейшая концептуальная деталь. В отличие от многих языков программирования, где условие — это специальное булево выражение (истина/ложь), в Bash условием для if служит код возврата (exit code) выполненной команды. Мы уже упоминали переменную $? в предыдущем уроке — она как раз содержит этот код. По соглашению Unix:

Это значит, что технически if может проверять результат любой команды, не только специальных «проверочных» конструкций:

```bash

if grep -q "root" /etc/passwd; then

echo "Строка 'root' найдена в файле"

fi

```

Здесь grep -q (тихий режим, -q — quiet, без вывода найденных строк на экран, только код возврата) вернёт 0, если совпадение найдено, и ненулевой код, если нет — и if проверяет именно это.

Команда test и её эквивалент [ ]. Для проверки типичных условий (сравнение чисел, строк, существование файлов) существует специальная команда test, которая, как и любая команда, тоже возвращает код 0 (истина) или ненулевой (ложь) в зависимости от результата проверки. Квадратная скобка [ — это, как ни удивительно, тоже команда, синоним test (с обязательной закрывающей ] в конце как частью синтаксиса этой команды):

```bash

if [ УСЛОВИЕ ]; then

...

fi

```

Важно: пробелы внутри [ ] обязательны[ $a -eq $b ] работает, а [$a -eq $b] (без пробелов после [ и перед ]) выдаст ошибку синтаксиса, потому что [ — это отдельная команда, и ей, как любой команде, нужны пробелы, отделяющие её от аргументов.

Операторы сравнения чисел (используются с test/[ ]):

| Оператор | Значение |

|---|---|

| -eq | equal, равно |

| -ne | not equal, не равно |

| -lt | less than, меньше |

| -le | less or equal, меньше или равно |

| -gt | greater than, больше |

| -ge | greater or equal, больше или равно |

Пример: if [ "$возраст" -ge 18 ]; then echo "Совершеннолетний"; fi

Операторы сравнения строк:

| Оператор | Значение |

|---|---|

| = (или ==) | строки равны |

| != | строки не равны |

| -z СТРОКА | строка пуста (zero length) |

| -n СТРОКА | строка не пуста |

Важная ловушка: для чисел используются -eq/-lt/-gt и т.д., а для строк — =/!=. Использование = для сравнения чисел или -eq для строк — частая ошибка новичков, приводящая к неожиданному поведению.

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

| Оператор | Значение |

|---|---|

| -e ФАЙЛ | файл существует (любого типа) |

| -f ФАЙЛ | существует и является обычным файлом |

| -d ФАЙЛ | существует и является директорией |

| -r ФАЙЛ | существует и доступен для чтения |

| -w ФАЙЛ | существует и доступен для записи |

| -x ФАЙЛ | существует и доступен для выполнения |

| -s ФАЙЛ | существует и не пустой (размер больше 0) |

Современный синтаксис [[ ]] — расширенная проверочная конструкция Bash. Помимо классической POSIX-совместимой [ ] (она же test), Bash предоставляет собственную, более мощную и удобную конструкцию [[ ]]:

```bash

if [[ УСЛОВИЕ ]]; then

...

fi

```

Преимущества [[ ]] перед [ ]:

  1. Не требует кавычек вокруг переменных для защиты от пустых значений и «слов, содержащих пробелы» так строго, как [ ] (хотя хорошая практика — заключать в кавычки в любом случае).
  2. Поддерживает более удобный синтаксис логических операторов внутри самой конструкции: && (И) и || (ИЛИ) прямо внутри [[ ]], тогда как в классическом [ ] для этого нужны отдельные конструкции -a/-o (устаревшие) или объединение нескольких [ ] через внешние &&/||.
  3. Поддерживает сопоставление с шаблоном (== внутри [[ ]] поддерживает символы-джокеры *, ?, похожие на те, что мы использовали для имён файлов в Блоке 5) и с регулярными выражениями через оператор =~ (мы подробно разберём регулярные выражения в уроке 15.8).

Логические операторы для объединения условий:

```bash

if [[ -f "$файл" && -r "$файл" ]]; then

echo "Файл существует и доступен для чтения"

fi

```

Рекомендация для этого курса: мы будем преимущественно использовать [[ ]] как более современный и безопасный вариант, но важно уметь узнавать и понимать классический [ ]/test, поскольку он повсеместно встречается в существующих скриптах, особенно если скрипт должен быть переносим на системы без Bash (используя только /bin/sh).

Практика

Шаг 1. Создайте базовый скрипт с условием:

```bash

cd ~/scripts

nano check_number.sh

```

Содержимое:

```bash

#!/bin/bash

число="${1:-0}"

if [[ $число -gt 0 ]]; then

echo "$число — положительное число"

elif [[ $число -lt 0 ]]; then

echo "$число — отрицательное число"

else

echo "$число — это ноль"

fi

```

```bash

chmod +x check_number.sh

./check_number.sh 5

./check_number.sh -3

./check_number.sh 0

```

Что вы увидите: соответствующее сообщение для каждого случая.

[Скриншот терминала]

Шаг 2. Создайте скрипт, проверяющий существование и тип файла — очень частая задача в реальном администрировании:

```bash

nano check_file.sh

```

Содержимое:

```bash

#!/bin/bash

путь="${1}"

if [[ -z "$путь" ]]; then

echo "Использование: ./check_file.sh /путь/к/файлу"

exit 1

fi

if [[ ! -e "$путь" ]]; then

echo "'$путь' не существует."

elif [[ -d "$путь" ]]; then

echo "'$путь' — это директория."

elif [[ -f "$путь" ]]; then

if [[ -s "$путь" ]]; then

echo "'$путь' — это непустой обычный файл."

else

echo "'$путь' — это пустой файл."

fi

else

echo "'$путь' существует, но это не обычный файл и не директория."

fi

```

```bash

chmod +x check_file.sh

./check_file.sh /etc/passwd

./check_file.sh /tmp

./check_file.sh /несуществующий/путь

touch /tmp/empty_test.txt

./check_file.sh /tmp/empty_test.txt

```

Что вы увидите: соответствующие сообщения для каждой ситуации — обычный файл, директория, несуществующий путь, пустой файл.

[Скриншот терминала со всеми вариантами]

Шаг 3. Практика с проверкой прав доступа (полезно вспомнить урок 8.6):

```bash

nano check_permissions.sh

```

Содержимое:

```bash

#!/bin/bash

путь="${1}"

if [[ -r "$путь" && -w "$путь" ]]; then

echo "У вас есть права на чтение И запись для '$путь'"

elif [[ -r "$путь" ]]; then

echo "У вас есть право только на чтение для '$путь'"

else

echo "У вас нет доступа для чтения к '$путь'"

fi

```

```bash

chmod +x check_permissions.sh

./check_permissions.sh /etc/passwd

./check_permissions.sh /etc/shadow

```

Что вы увидите: разные результаты — /etc/passwd обычно доступен для чтения всем, а /etc/shadow (мы обсуждали этот особый файл в Блоке 8) — нет, если вы не root.

[Скриншот терминала]

Шаг 4. Создайте практический скрипт, применяющий материал из Блока 14 (безопасность) — проверку состояния firewall:

```bash

nano check_security_status.sh

```

Содержимое:

```bash

#!/bin/bash

статус_ufw=$(sudo ufw status | head -1)

if [[ "$статус_ufw" == "active" ]]; then

echo "✓ Firewall активен"

else

echo "✗ ВНИМАНИЕ: firewall не активен!"

fi

if systemctl is-active --quiet fail2ban; then

echo "✓ Fail2Ban работает"

else

echo "✗ ВНИМАНИЕ: Fail2Ban не запущен!"

fi

```

Обратите внимание на использование == с шаблоном "active" внутри [[ ]] — это пример сопоставления с шаблоном, доступного именно в [[ ]] (в классическом [ ] так работать не будет).

```bash

chmod +x check_security_status.sh

./check_security_status.sh

```

[Скриншот результата проверки безопасности]

Шаг 5. Сравните классический синтаксис [ ] с современным [[ ]] на практике — намеренно вызовите ошибку с непроверенной пустой переменной:

```bash

nano old_vs_new.sh

```

Содержимое:

```bash

#!/bin/bash

переменная=""

echo "--- Тест с [ ] без кавычек (может вызвать ошибку) ---"

if [ $переменная = "тест" ]; then

echo "Совпадает"

else

echo "Не совпадает"

fi

echo "--- Тест с [[ ]] без кавычек (работает надёжнее) ---"

if [[ $переменная = "тест" ]]; then

echo "Совпадает"

else

echo "Не совпадает"

fi

```

```bash

chmod +x old_vs_new.sh

./old_vs_new.sh

```

Что вы увидите: первый блок ([ ]) выдаст ошибку синтаксиса (unary operator expected), потому что пустая переменная без кавычек «исчезает» и [ получает недостаточно аргументов. Второй блок ([[ ]]) отработает корректно даже без явных кавычек, потому что [[ ]] — это специальная конструкция самого языка Bash с более надёжной обработкой таких случаев.

[Скриншот терминала, демонстрирующий разницу]

Разбор команд

Конструкция if/elif/else/fi

```bash

if условие1; then

команды1

elif условие2; then

команды2

else

команды3

fi

```

test / [ ] / [[ ]]

Возможные ошибки

Почему возникает: переменная внутри [ ] не заключена в кавычки, и оказывается пустой или содержит несколько слов, из-за чего команда [ получает неожиданное количество аргументов.

Как исправить: заключать переменные в кавычки: [ "$переменная" = "значение" ], или использовать [[ ]], которая устойчивее к этой проблеме.

Почему возникает: путаница между операторами для чисел и строк.

Как определить: сравнение работает не так, как ожидалось (например, "10" = "9" при строковом сравнении может вести себя иначе, чем ожидалось при работе с числами, а -eq с нечисловой строкой выдаст ошибку integer expression expected).

Как исправить/избежать: чётко помнить: числа — -eq/-ne/-lt/-le/-gt/-ge; строки — =/!=.

Практические задания

  1. Напишите скрипт check_age.sh, принимающий возраст как аргумент, и выводящий: «Ребёнок» (0–12), «Подросток» (13–17), «Взрослый» (18–64), «Пенсионер» (65+), используя цепочку if/elif/else.
  2. Напишите скрипт disk_space_alert.sh, который проверяет процент занятого места на корневом разделе (df / --output=pcent | tail -1 вернёт число с процентом — потребуется дополнительная обработка текста, например через tr -d '%' для удаления символа %, который мы изучали в Блоке 6) и выводит предупреждение, если занято больше 80%.
  3. Изучите на своей системе (без выполнения, только через man test или поиск в интернете) дополнительные операторы проверки файлов, которые мы не разбирали (-L для симлинков, -nt/-ot для сравнения времени модификации двух файлов), и опишите в конспекте, для какой практической задачи каждый из них мог бы пригодиться.

Проверка знаний

Вопрос 1. Что на самом деле проверяет конструкция if, если не «истину» и «ложь» в привычном математическом смысле?

Ответ: if проверяет код возврата (exit code) выполненной команды. Код 0 интерпретируется как «успех»/«истина», любой ненулевой код — как «ошибка»/«ложь». Это относится к любой команде, не только к специальным проверочным конструкциям test/[ ]/[[ ]] — можно использовать if с любой командой, включая grep, ping, собственные скрипты и так далее.

Вопрос 2. Почему [[ ]] считается более безопасным вариантом по сравнению с классическим [ ], особенно при работе с переменными, которые могут быть пустыми?

Ответ: [[ ]] — это специальная синтаксическая конструкция самого языка Bash (а не отдельная команда, как [/test), поэтому Bash обрабатывает содержимое внутри неё более интеллектуально: пустая или содержащая пробелы переменная не «разваливается» на несколько аргументов и не приводит к ошибкам синтаксиса вида unary operator expected, даже без явного заключения в кавычки — хотя заключение в кавычки всё равно остаётся хорошей практикой для ясности кода.

Итоги урока

Вы изучили: синтаксис if/elif/else/fi, концепцию кода возврата как основы условий в Bash, операторы сравнения чисел и строк, операторы проверки файлов, разницу между [ ] и [[ ]], логические операторы &&/||/!.

Вы умеете: писать скрипты с условной логикой, проверять числа, строки, существование и свойства файлов, комбинировать условия.

В следующем уроке потребуется: понимание условий — теперь мы научимся повторять действия многократно с помощью циклов, часто в сочетании с условиями.

Урок 15.5. Циклы: `for`, `while`, `until`

Освоить три типа циклов в Bash — for (перебор элементов), while (повторение, пока условие истинно) и until (повторение, пока условие ложно), и научиться применять их для типичных задач автоматизации.

Теория

Зачем нужны циклы. Циклы позволяют выполнить один и тот же блок команд многократно — либо для каждого элемента из некоторого набора (списка файлов, слов, чисел), либо пока выполняется (или не выполняется) определённое условие. Без циклов пришлось бы вручную дублировать код для обработки, скажем, каждого из ста файлов в директории — с циклом это делается несколькими строками, независимо от того, сколько на самом деле файлов.

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

Базовый синтаксис:

```bash

for переменная in список_элементов

do

команды с использованием $переменная

done

```

Или в компактной форме:

```bash

for переменная in список_элементов; do

команды

done

```

Обязательное завершение — done.

Разные формы источника элементов для for:

  1. Явный список слов:

```bash

for цвет in красный зелёный синий; do

echo "Цвет: $цвет"

done

```

  1. Результат выполнения команды (через уже знакомую нам подстановку $(...)):

```bash

for файл in $(ls *.txt); do

echo "Обрабатываю: $файл"

done

```

Важное предупреждение: этот способ (перебор через $(ls ...)) считается небезопасной практикой, если имена файлов могут содержать пробелы — потому что при подстановке команды Bash разбивает результат на «слова» именно по пробелам, ломая имена файлов с пробелами на несколько отдельных «элементов». Более безопасная альтернатива для перебора файлов — использование шаблонов подстановки имён файлов (см. следующий пункт).

  1. Шаблоны подстановки имён файлов (glob-паттерны) — безопаснее и предпочтительнее для перебора файлов:

```bash

for файл in *.txt; do

echo "Обрабатываю: $файл"

done

```

Здесь .txt разворачивается самим Bash в список файлов с этим расширением в текущей директории (тот же механизм подстановки имён файлов, что мы использовали ранее с ls .txt в Блоке 5), и каждое имя файла становится отдельным, корректно обработанным элементом цикла, даже если имя содержит пробелы (при условии, что вы не забыли заключить переменную в кавычки при дальнейшем использовании: "$файл").

  1. Диапазон чисел:

```bash

for i in {1..5}; do

echo "Итерация номер $i"

done

```

Конструкция {1..5} — это встроенное расширение диапазона Bash (brace expansion), генерирующее последовательность 1 2 3 4 5. Можно также задать шаг: {1..10..2} даст 1 3 5 7 9.

  1. C-подобный синтаксис (для тех, кто знаком с другими языками программирования, похож на классический цикл for в C или Java):

```bash

for (( i=1; i<=5; i++ )); do

echo "Итерация номер $i"

done

```

  1. Перебор аргументов скрипта (применение материала предыдущего урока):

```bash

for аргумент in "$@"; do

echo "Получен аргумент: $аргумент"

done

```

Цикл while — повторение, пока условие истинно.

```bash

while условие; do

команды

done

```

Цикл проверяет условие перед каждой итерацией; если условие истинно (код возврата 0) — тело цикла выполняется, затем проверка повторяется. Как только условие становится ложным — цикл завершается.

Классическое применение while — чтение файла построчно:

```bash

while read -r строка; do

echo "Прочитана строка: $строка"

done < файл.txt

```

Здесь < файл.txt — перенаправление ввода (мы изучали перенаправления в Блоке 6) — файл построчно «скармливается» команде read внутри цикла. Ключ -r у read предотвращает интерпретацию обратных слэшей \ как специальных символов — почти всегда стоит его использовать при чтении произвольного текста.

Цикл while true — бесконечный цикл с условием выхода внутри:

```bash

while true; do

команды

if условие_завершения; then

break

fi

done

```

Такая конструкция часто применяется для скриптов мониторинга, которые должны работать непрерывно, периодически проверяя что-то (например, состояние службы), пока не случится определённое событие. Оператор break (разберём подробнее ниже) немедленно прерывает цикл.

Цикл until — повторение, пока условие ложно (противоположность while).

```bash

until условие; do

команды

done

```

until выполняет тело цикла до тех пор, пока условие не станет истинным — логика, обратная while. На практике until используется реже, чем while (почти любую задачу с until можно переформулировать через while с инвертированным условием, используя !), но иногда делает код более читаемым — например, «повторяй попытки подключения, пока не подключишься успешно»:

```bash

until ping -c 1 8.8.8.8 &> /dev/null; do

echo "Сеть недоступна, повторная попытка через 5 секунд..."

sleep 5

done

echo "Сеть доступна!"

```

(Мы использовали ping для проверки связи ещё в Блоке 12; &> /dev/null подавляет весь вывод команды, перенаправляя и стандартный вывод, и вывод ошибок в «чёрную дыру» /dev/null, о которой мы тоже говорили в Блоке 6 — нас интересует только код возврата ping, а не её текстовый вывод.)

Управляющие операторы внутри циклов:

```bash

for i in {1..10}; do

if [[ $i -eq 5 ]]; then

continue # пропустить число 5, но продолжить цикл дальше

fi

if [[ $i -eq 8 ]]; then

break # полностью остановить цикл на числе 8

fi

echo $i

done

```

Этот пример выведет 1 2 3 4 6 7 — число 5 будет пропущено (continue), а после 7 цикл остановится полностью, не дойдя до 8 (break).

Практика

Шаг 1. Создайте скрипт с базовым for по явному списку:

```bash

cd ~/scripts

nano for_basic.sh

```

Содержимое:

```bash

#!/bin/bash

echo "--- Перебор явного списка ---"

for город in Рига Вильнюс Таллин Хельсинки; do

echo "Город: $город"

done

echo "--- Перебор диапазона чисел ---"

for i in {1..5}; do

echo "Число: $i"

done

echo "--- C-подобный синтаксис ---"

for (( i=0; i<5; i++ )); do

echo "Индекс: $i"

done

```

```bash

chmod +x for_basic.sh

./for_basic.sh

```

[Скриншот терминала]

Шаг 2. Создайте практический скрипт для безопасного перебора файлов:

```bash

mkdir -p ~/scripts/test_files

touch ~/scripts/test_files/{report1.txt,report2.txt,"my report 3.txt"}

nano for_files.sh

```

Содержимое:

```bash

#!/bin/bash

директория="$HOME/scripts/test_files"

echo "--- Файлы в директории (безопасный перебор через glob) ---"

for файл in "$директория"/*.txt; do

echo "Найден файл: $файл"

echo " Размер: $(stat -c%s "$файл") байт"

done

```

(Команда stat -c%s выводит размер файла в байтах — stat мы кратко упоминали в контексте прав доступа в Блоке 8.)

```bash

chmod +x for_files.sh

./for_files.sh

```

Что вы увидите: корректная обработка всех трёх файлов, включая тот, у которого имя содержит пробелы (my report 3.txt) — потому что мы использовали кавычки вокруг "$файл".

[Скриншот терминала]

Шаг 3. Создайте скрипт с while, читающий файл построчно:

```bash

echo -e "первая строка\nвторая строка\nтретья строка" > ~/scripts/sample.txt

nano while_read.sh

```

Содержимое:

```bash

#!/bin/bash

номер=1

while read -r строка; do

echo "Строка $номер: $строка"

((номер++))

done < ~/scripts/sample.txt

echo "Всего обработано строк: $((номер - 1))"

```

(Конструкция ((номер++)) — это арифметическое выражение, увеличивающее переменную номер на 1.)

```bash

chmod +x while_read.sh

./while_read.sh

```

[Скриншот терминала]

Шаг 4. Создайте скрипт с while, реализующий счётчик с условием:

```bash

nano while_counter.sh

```

Содержимое:

```bash

#!/bin/bash

счётчик=1

while [[ $счётчик -le 5 ]]; do

echo "Попытка номер $счётчик"

((счётчик++))

done

echo "Цикл завершён, счётчик достиг $счётчик"

```

```bash

chmod +x while_counter.sh

./while_counter.sh

```

Шаг 5. Создайте практический скрипт с until, ожидающий доступности сети (или localhost, если внешняя сеть недоступна в вашей учебной среде):

```bash

nano until_wait.sh

```

Содержимое:

```bash

#!/bin/bash

попытка=1

максимум_попыток=3

until ping -c 1 -W 1 127.0.0.1 &> /dev/null; do

echo "Попытка $попытка: сервер недоступен, ждём..."

((попытка++))

if [[ $попытка -gt $максимум_попыток ]]; then

echo "Превышено максимальное число попыток. Выход."

exit 1

fi

sleep 2

done

echo "Сервер доступен! (проверено с попытки $попытка)"

```

```bash

chmod +x until_wait.sh

./until_wait.sh

```

Что вы увидите: поскольку localhost (127.0.0.1) всегда доступен, цикл until завершится сразу после первой успешной проверки.

[Скриншот терминала]

Шаг 6. Продемонстрируйте break и continue на практике:

```bash

nano break_continue.sh

```

Содержимое:

```bash

#!/bin/bash

echo "Демонстрация continue и break:"

for i in {1..10}; do

if (( i % 2 == 0 )); then

continue # пропустить чётные числа

fi

if (( i > 7 )); then

break # остановиться после превышения 7

fi

echo "Нечётное число: $i"

done

```

(Оператор % — остаток от деления, здесь используется для проверки чётности числа.)

```bash

chmod +x break_continue.sh

./break_continue.sh

```

Что вы увидите: 1 3 5 7 — чётные числа пропущены через continue, а после 7 цикл прерван через break, не дойдя до 9.

[Скриншот терминала]

Шаг 7. Объедините всё изученное в практический мини-скрипт — резервное копирование каждого файла из директории с добавлением расширения .bak:

```bash

mkdir -p ~/scripts/backup_demo

nano backup_files.sh

```

Содержимое:

```bash

#!/bin/bash

исходная_директория="$HOME/scripts/test_files"

резервная_директория="$HOME/scripts/backup_demo"

for файл in "$исходная_директория"/*; do

if [[ -f "$файл" ]]; then

имя_файла=$(basename "$файл")

cp "$файл" "$резервная_директория/${имя_файла}.bak"

echo "Скопирован: $имя_файла → ${имя_файла}.bak"

fi

done

echo "Резервное копирование завершено. Всего файлов: $(ls "$резервная_директория" | wc -l)"

```

(Команда basename извлекает только имя файла из полного пути, отбрасывая путь директории — полезная утилита, которую мы не упоминали ранее явно.)

```bash

chmod +x backup_files.sh

./backup_files.sh

ls ~/scripts/backup_demo/

```

[Скриншот результата резервного копирования]

Разбор команд

Цикл for

Цикл while

Цикл until

break и continue

Команда basename

Возможные ошибки

Почему возникает: подстановка команды $(ls ...) разбивается на слова по пробелам, разделяя одно имя файла на несколько «элементов» цикла.

Как определить: файл my report.txt в цикле обрабатывается как два отдельных элемента: my и report.txt.

Как исправить/избежать: использовать glob-паттерны напрямую (for файл in *.txt) вместо $(ls ...), и всегда заключать переменную файла в кавычки при дальнейшем использовании ("$файл").

Почему возникает: условие цикла никогда не становится ложным — например, забыли увеличить счётчик (((счётчик++))) внутри тела цикла.

Как определить: скрипт «зависает», не завершаясь, терминал продолжает работать без возврата приглашения командной строки.

Как исправить: прервать выполнение сочетанием Ctrl+C (мы упоминали эту комбинацию для завершения процессов в Блоке 11); внимательно проверить логику условия и убедиться, что оно действительно может стать ложным при нормальном ходе выполнения скрипта.

Как избежать: всегда перепроверять логику условия цикла перед запуском, особенно для while true — предусматривать явное условие или оператор break для выхода.

Практические задания

  1. Напишите скрипт, который переберёт числа от 1 до 20 через for с использованием {1..20}, и для каждого числа, кратного 3, выведет «Fizz», кратного 5 — «Buzz», кратного обоим — «FizzBuzz», а для остальных — само число (классическая учебная задача программирования — «FizzBuzz», подсказка: используйте % для проверки остатка от деления, как в шаге 6 практики).
  2. Напишите скрипт count_lines.sh, принимающий имя файла как аргумент ($1) и с помощью while read подсчитывающий и выводящий количество строк, содержащих слово «error» (без учёта регистра — вспомните флаг -i у grep, либо просто используйте grep -c напрямую вместо цикла, чтобы сравнить два подхода к одной задаче).
  3. Напишите скрипт, использующий until, который «дожидается» появления определённого файла (например, /tmp/ready.flag) — проверяя его существование каждые 2 секунды, максимум 5 попыток, и выводит сообщение об успехе или о превышении лимита попыток. Проверьте работу скрипта, вручную создав файл touch /tmp/ready.flag в отдельном терминале, пока первый терминал ожидает.

Проверка знаний

Вопрос 1. Почему перебор файлов через for файл in $(ls директория) считается небезопасной практикой, и какая альтернатива предпочтительнее?

Ответ: Подстановка команды $(ls директория) возвращает список имён файлов как единую строку, которая затем разбивается Bash на отдельные «слова» по пробелам (и другим символам-разделителям). Если имя файла содержит пробел, оно будет разбито на несколько отдельных элементов цикла, что приведёт к некорректной обработке. Более безопасная альтернатива — использование glob-паттернов напрямую: for файл in директория/*, где Bash сам корректно разворачивает шаблон в список полных, неразбитых имён файлов, при условии что переменная затем используется в кавычках.

Вопрос 2. В чём разница между break и continue внутри цикла?

Ответ: break полностью завершает выполнение цикла — все оставшиеся итерации (для оставшихся элементов или пока условие ещё истинно) не выполняются, управление передаётся коду после done. continue завершает только текущую итерацию — код после continue в текущем проходе цикла не выполняется, но цикл переходит к следующей итерации (следующему элементу в for или следующей проверке условия в while/until), а не завершается полностью.

Итоги урока

Вы изучили: синтаксис циклов for (с разными источниками элементов), while (включая классическое построчное чтение файла), until, управляющие операторы break и continue, команды basename/dirname.

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

В следующем уроке потребуется: понимание циклов и условий — теперь мы научимся упаковывать повторяющуюся логику в переиспользуемые функции.

Урок 15.6. Функции в bash

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

Теория

Зачем нужны функции. По мере роста скрипта одни и те же последовательности команд могут повторяться в разных местах — например, форматированный вывод сообщения об ошибке, или проверка наличия определённой утилиты. Копирование одного и того же кода в нескольких местах (то, что программисты называют «дублированием кода») делает скрипт длиннее, сложнее для чтения и, что важнее, сложнее для поддержки: если понадобится исправить логику, придётся искать и исправлять её во всех местах, где она продублирована, рискуя что-то упустить. Функция — это именованный, переиспользуемый блок кода, который можно определить один раз и затем вызывать многократно из разных мест скрипта.

Синтаксис объявления функции. В Bash есть два эквивалентных синтаксиса:

```bash

имя_функции() {

команды

}

function имя_функции {

команды

}

```

Мы будем придерживаться первого, более распространённого варианта.

Вызов функции — просто по имени, как обычную команду:

```bash

имя_функции

```

Важно: функцию нужно объявить до её первого вызова — Bash читает скрипт последовательно сверху вниз, поэтому определение функции должно физически находиться в файле раньше места, где она вызывается (обычная практика — размещать все объявления функций в начале скрипта, а основную логику — ниже).

Аргументы функции. Функции в Bash принимают аргументы точно так же, как сам скрипт принимает аргументы командной строки — через уже знакомые нам $1, $2, $#, $@ — но внутри функции эти переменные относятся к аргументам, переданным именно этой функции, а не всему скрипту:

```bash

приветствие() {

echo "Привет, $1! У тебя $2 новых сообщений."

}

приветствие "Анна" 5

```

Здесь при вызове приветствие "Анна" 5, внутри функции $1 будет равен "Анна", а $25 — независимо от того, какие аргументы были переданы самому скрипту при его запуске.

Возврат значений из функций — важная особенность Bash. В отличие от многих языков программирования, функции в Bash не могут напрямую «вернуть» произвольное значение (например, строку) так, как это делают функции в Python или JavaScript. У функций Bash есть два способа «вернуть» результат:

  1. Код возврата через return — но, как и код возврата всего скрипта ($?, изученный в предыдущих уроках), это только число от 0 до 255, по соглашению используемое для обозначения успеха (0) или разных видов ошибки (не 0). Это не предназначено для возврата, например, вычисленного текста или числа с содержательным значением, только для индикации успеха/неуспеха:

```bash

проверить_чётность() {

if (( $1 % 2 == 0 )); then

return 0 # успех = чётное

else

return 1 # неуспех = нечётное

fi

}

if проверить_чётность 4; then

echo "Число чётное"

fi

```

  1. Вывод через echo и захват через $(...) — если нужно «вернуть» содержательное значение (строку, число, результат вычисления), стандартная практика в Bash — вывести это значение через echo внутри функции, а затем захватить этот вывод при вызове функции через уже знакомую нам подстановку команд $(...):

```bash

получить_дату_форматированную() {

echo "$(date +%d.%m.%Y)"

}

сегодня=$(получить_дату_форматированную)

echo "Сегодняшняя дата: $сегодня"

```

Это, по сути, тот же механизм, что мы использовали для захвата вывода обычных команд системы ($(date), $(whoami)) — функция в этом смысле ведёт себя как ещё одна команда.

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

Ключевое слово local объявляет переменную локальной для функции — она существует только внутри этой функции и автоматически удаляется после её завершения, не затрагивая одноимённые переменные снаружи:

```bash

демо_локальной_переменной() {

local внутренняя="я существую только внутри функции"

echo "Внутри функции: $внутренняя"

}

демо_локальной_переменной

echo "Снаружи функции: '$внутренняя'"

```

Второй echo выведет пустую строку — переменная внутренняя, объявленная с local, не существует за пределами функции. Хорошая практика — почти всегда использовать local для переменных, создаваемых внутри функций, чтобы избежать случайного «загрязнения» глобального пространства имён переменных всего скрипта — особенно важно в больших скриптах со множеством функций.

Функции как способ организации скрипта. Хорошо организованный bash-скрипт обычно имеет структуру:

```bash

#!/bin/bash

функция1() { ... }

функция2() { ... }

функция1

if условие; then

функция2

fi

```

Такая структура делает скрипт значительно более читаемым: основная логика внизу файла читается почти как «оглавление» действий, а детали реализации каждого шага скрыты внутри соответствующей функции.

Практика

Шаг 1. Создайте скрипт с простыми функциями:

```bash

cd ~/scripts

nano functions_basic.sh

```

Содержимое:

```bash

#!/bin/bash

приветствие() {

echo "Здравствуйте! Это функция приветствия."

}

приветствие_по_имени() {

echo "Привет, $1! Рады тебя видеть."

}

сумма() {

local результат=$(( $1 + $2 ))

echo "$результат"

}

приветствие

приветствие_по_имени "Мария"

итог=$(сумма 5 7)

echo "Сумма 5 и 7 равна: $итог"

```

```bash

chmod +x functions_basic.sh

./functions_basic.sh

```

[Скриншот терминала]

Шаг 2. Продемонстрируйте разницу между локальными и глобальными переменными:

```bash

nano local_vs_global.sh

```

Содержимое:

```bash

#!/bin/bash

переменная="глобальное значение"

изменить_без_local() {

переменная="изменено функцией (без local)"

}

изменить_с_local() {

local переменная="изменено функцией (с local)"

echo "Внутри функции с local: $переменная"

}

echo "До вызова функций: $переменная"

изменить_без_local

echo "После изменить_без_local: $переменная"

изменить_с_local

echo "После изменить_с_local: $переменная"

```

```bash

chmod +x local_vs_global.sh

./local_vs_global.sh

```

Что вы увидите: после изменить_без_local глобальная переменная действительно изменится («изменено функцией (без local)»), а после изменить_с_local — останется прежним значением, потому что локальная переменная внутри функции «спрятала» глобальную только на время выполнения функции, не изменив её на самом деле.

[Скриншот терминала, демонстрирующий разницу]

Шаг 3. Создайте скрипт с функцией, возвращающей код через return, применённой в условии:

```bash

nano return_demo.sh

```

Содержимое:

```bash

#!/bin/bash

является_числом() {

if [[ $1 =~ ^[0-9]+$ ]]; then

return 0

else

return 1

fi

}

for значение in "123" "привет" "456" "78x"; do

if является_числом "$значение"; then

echo "'$значение' — это число"

else

echo "'$значение' — НЕ число"

fi

done

```

```bash

chmod +x return_demo.sh

./return_demo.sh

```

[Скриншот терминала]

Шаг 4. Создайте практический скрипт-библиотеку функций для проверки безопасности (применение материала Блока 14 через функции):

```bash

nano security_functions.sh

```

Содержимое:

```bash

#!/bin/bash

проверить_службу() {

local имя_службы="$1"

if systemctl is-active --quiet "$имя_службы"; then

echo "✓ Служба '$имя_службы' активна"

return 0

else

echo "✗ Служба '$имя_службы' НЕ активна"

return 1

fi

}

проверить_порт_открыт() {

local порт="$1"

if sudo ss -tlnp | grep -q ":$порт "; then

echo "✓ Порт $порт открыт и прослушивается"

else

echo "✗ Порт $порт не прослушивается"

fi

}

вывести_разделитель() {

echo "=================================="

}

вывести_разделитель

echo "Проверка ключевых служб безопасности:"

вывести_разделитель

проверить_службу "ssh"

проверить_службу "fail2ban"

проверить_службу "ufw"

вывести_разделитель

echo "Проверка ключевых портов:"

вывести_разделитель

проверить_порт_открыт "22"

вывести_разделитель

```

```bash

chmod +x security_functions.sh

./security_functions.sh

```

[Скриншот полного результата проверки]

Шаг 5. Продемонстрируйте важность объявления функции до её вызова:

```bash

nano order_matters.sh

```

Содержимое:

```bash

#!/bin/bash

моя_функция

моя_функция() {

echo "Я определена ниже места вызова"

}

```

```bash

chmod +x order_matters.sh

./order_matters.sh

```

Что вы увидите: ошибку моя_функция: command not found — потому что на момент вызова (первая строка после shebang) Bash ещё не «знает» об этой функции, так как её объявление находится ниже по файлу.

Шаг 6. Исправьте скрипт, поменяв порядок:

```bash

nano order_fixed.sh

```

Содержимое:

```bash

#!/bin/bash

моя_функция() {

echo "Теперь я определена выше места вызова"

}

моя_функция

```

```bash

chmod +x order_fixed.sh

./order_fixed.sh

```

[Скриншот, демонстрирующий исправленный результат]

Разбор команд

Объявление функции

Ключевое слово local

Оператор return

Возможные ошибки

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

Как исправить: переместить объявление функции выше места её первого вызова; сверить точное написание имени.

Почему возникает: непонимание того, что return предназначен только для кодов от 0 до 255, обозначающих успех/ошибку, а не для произвольных числовых или текстовых результатов.

Как исправить: использовать echo внутри функции и $(функция) при вызове для получения содержательного результата; return использовать исключительно для индикации успеха (0) или разных видов ошибки (1–255).

Почему возникает: переменная внутри функции была объявлена без local, из-за чего изменила глобальную переменную с тем же именем.

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

Практические задания

  1. Напишите функцию является_root(), которая проверяет, запущен ли скрипт от имени пользователя root (подсказка: сравните $(whoami) со строкой "root", либо изучите переменную $EUID, которая содержит числовой идентификатор текущего эффективного пользователя — у root он всегда 0), и использующую return для индикации результата. В основной части скрипта используйте эту функцию в if, чтобы вывести предупреждение, если скрипт запущен НЕ от root.
  2. Напишите функцию конвертировать_в_мб(), принимающую число байт как аргумент и возвращающую (через echo) это число, переведённое в мегабайты (разделив на 1048576 — используйте арифметическое расширение $(( ))). Продемонстрируйте её работу, захватив результат через $(...) для нескольких разных входных значений.
  3. Перепишите скрипт security_check.sh из урока 14.6, реорганизовав его в набор из 3–4 отдельных функций (например, проверить_ufw(), проверить_fail2ban(), показать_последние_входы()) и одной основной секции, вызывающей эти функции по порядку. Сравните читаемость нового варианта с исходным.

Проверка знаний

Вопрос 1. Почему нельзя использовать return в Bash-функции для возврата, например, вычисленной строки текста или произвольного числа, и как правильно решить эту задачу?

Ответ: return в Bash предназначен исключительно для установки кода возврата функции — числа от 0 до 255, по соглашению обозначающего успех (0) или различные виды ошибки (не 0). Он не предназначен для передачи содержательных данных. Чтобы «вернуть» текстовое или числовое значение из функции, нужно вывести его через echo внутри функции, а затем захватить этот вывод при вызове функции через подстановку команд: результат=$(имя_функции аргументы).

Вопрос 2. Что произойдёт, если переменная внутри функции объявлена без ключевого слова local, и как это может привести к трудноуловимым ошибкам в большом скрипте?

Ответ: Без local переменная, объявленная внутри функции, становится глобальной — доступной и изменяемой из любой другой части скрипта, включая другие функции. Если в разных функциях случайно используются одинаковые имена переменных без local, одна функция может незаметно изменить значение переменной, которое затем неожиданно используется другой частью скрипта — такие ошибки трудно найти, потому что причина (изменение переменной) и следствие (неожиданное поведение в другом месте) могут быть физически далеко друг от друга в коде скрипта.

Итоги урока

Вы изучили: синтаксис объявления и вызова функций, передачу аргументов функциям ($1, $2... внутри функции), два способа «возврата» результата (return для кода, echo+$(...) для содержательных данных), ключевое слово local и область видимости переменных.

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

В следующем уроке потребуется: понимание функций и условий — теперь мы сосредоточимся на надёжности скриптов: правильной работе с файлами и грамотной обработке ошибок.

Урок 15.7. Работа с файлами и обработка ошибок

Научиться писать надёжные скрипты: корректно читать, создавать и проверять файлы, применять коды возврата и set -e/set -u для раннего обнаружения ошибок, использовать перенаправления и trap для аккуратной очистки ресурсов.

Теория

Почему обработка ошибок критически важна для скриптов. Скрипт, который «просто работает» в идеальных условиях, но ломается непредсказуемым образом при малейшем отклонении (файл не найден, недостаточно прав, нет места на диске) — плохой, ненадёжный инструмент, особенно если он выполняется автоматически без присмотра (что мы изучим в следующем, последнем уроке блока — про cron). Хороший скрипт должен либо корректно обработать проблему, либо чётко и понятно сообщить о ней и безопасно остановиться, не оставляя систему в неопределённом или повреждённом состоянии.

Коды возврата — фундамент обработки ошибок в Bash. Мы уже неоднократно упоминали $? — переменную, содержащую код возврата последней выполненной команды. Хороший скрипт должен активно проверять эти коды, а не слепо продолжать выполнение, предполагая, что всё прошло успешно:

```bash

cp важный_файл.txt /backup/

if [[ $? -ne 0 ]]; then

echo "Ошибка: не удалось скопировать файл!" >&2

exit 1

fi

```

(Мы уже видели >&2 — перенаправление в поток стандартной ошибки, изученное в Блоке 6 — сообщения об ошибках правильно направлять именно туда, а не в обычный стандартный вывод.)

Более идиоматичный (характерный для стиля Bash) способ проверки — напрямую использовать команду в условии if, без промежуточной проверки $?:

```bash

if ! cp важный_файл.txt /backup/; then

echo "Ошибка: не удалось скопировать файл!" >&2

exit 1

fi

```

Символ ! перед командой инвертирует её код возврата — «если команда не выполнилась успешно».

Команда exit и коды завершения скрипта. Точно так же, как функции возвращают код через return, весь скрипт целиком возвращает код через exit:

```bash

exit 0 # скрипт завершился успешно

exit 1 # скрипт завершился с общей ошибкой

```

По соглашению используются разные ненулевые коды для разных типов ошибок в сложных скриптах (например, exit 2 для «неверные аргументы», exit 3 для «файл не найден» и т.д.) — это позволяет другим скриптам или системам, вызывающим ваш скрипт, программно различать разные причины неудачи по коду возврата, а не только по тексту сообщения.

Режимы строгости: set -e, set -u, set -o pipefail. Bash по умолчанию не останавливается при ошибке в отдельной команде — если команда завершилась с ошибкой, скрипт просто продолжает выполнение следующей строки, что может привести к каскаду дальнейших проблем (например, попытке обработать файл, который не удалось создать на предыдущем шаге). Специальная команда set с определёнными опциями меняет это поведение по умолчанию:

Стандартная, широко рекомендуемая связка в начале серьёзных скриптов:

```bash

#!/bin/bash

set -euo pipefail

```

(Три опции объединены в одну строку — распространённое сокращение.)

Эта строка резко повышает надёжность скрипта: любая непредвиденная ошибка немедленно останавливает выполнение, вместо того чтобы позволить скрипту продолжать работу в потенциально повреждённом состоянии.

trap — перехват сигналов и гарантированная очистка. Иногда скрипту нужно гарантированно выполнить определённые действия при завершении — независимо от того, завершился ли он успешно, с ошибкой, или был прерван пользователем (например, через Ctrl+C, который отправляет процессу сигнал, о сигналах мы говорили в Блоке 11). Классический пример — удаление временных файлов, созданных скриптом, даже если скрипт прервался раньше времени из-за ошибки.

```bash

trap 'команда_очистки' EXIT

```

EXIT — специальное «псевдо-событие» в Bash, которое срабатывает при любом завершении скрипта — по любой причине (нормальное завершение, exit, ошибка при set -e, получение сигнала прерывания).

```bash

временный_файл=$(mktemp)

trap 'rm -f "$временный_файл"' EXIT

echo "Работаем с временным файлом $временный_файл"

```

(Команда mktemp создаёт уникальный временный файл с гарантированно не занятым именем в /tmp и выводит его путь — более безопасная альтернатива самостоятельному конструированию имени через $$, которое мы использовали в уроке 15.3, хотя оба подхода встречаются на практике.)

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

```bash

if ! command -v rsync &> /dev/null; then

echo "Ошибка: требуется утилита rsync, но она не установлена." >&2

exit 1

fi

```

(command -v проверяет, существует ли команда с данным именем в $PATH, не выполняя её — стандартный и переносимый способ такой проверки.)

Практика

Шаг 1. Создайте скрипт, демонстрирующий разницу в поведении с set -e и без него:

```bash

cd ~/scripts

nano error_demo.sh

```

Содержимое:

```bash

#!/bin/bash

echo "Пробуем скопировать несуществующий файл..."

cp /несуществующий/файл.txt /tmp/ 2>/dev/null

echo "Эта строка выполнится, даже если копирование выше провалилось!"

echo "Скрипт дошёл до конца без остановки."

```

```bash

chmod +x error_demo.sh

./error_demo.sh

```

Что вы увидите: несмотря на ошибку копирования, скрипт продолжает выполнение и доходит до конца — это поведение Bash по умолчанию.

Шаг 2. Добавьте set -e и посмотрите на изменившееся поведение:

```bash

nano error_demo_strict.sh

```

Содержимое:

```bash

#!/bin/bash

set -e

echo "Пробуем скопировать несуществующий файл..."

cp /несуществующий/файл.txt /tmp/

echo "Эта строка НЕ должна выполниться, если set -e работает правильно!"

```

```bash

chmod +x error_demo_strict.sh

./error_demo_strict.sh

```

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

[Скриншот терминала, демонстрирующий разницу]

Шаг 3. Продемонстрируйте set -u на примере опечатки в имени переменной:

```bash

nano unset_demo.sh

```

Содержимое:

```bash

#!/bin/bash

set -u

имя_файла="report.txt"

echo "Обрабатываем файл: $имя_фала" # намеренная опечатка: "фала" вместо "файла"

```

```bash

chmod +x unset_demo.sh

./unset_demo.sh

```

Что вы увидите: ошибку имя_фала: unbound variableset -u немедленно поймал использование неопределённой (из-за опечатки) переменной, вместо того чтобы молча подставить пустую строку.

[Скриншот терминала с пойманной ошибкой]

Шаг 4. Создайте практический скрипт с проверкой команд, файлов и правильными кодами возврата — резервное копирование с полноценной обработкой ошибок:

```bash

nano safe_backup.sh

```

Содержимое:

```bash

#!/bin/bash

set -euo pipefail

источник="${1:-}"

назначение="${2:-}"

if [[ -z "$источник" || -z "$назначение" ]]; then

echo "Использование: $0 <источник> <назначение>" >&2

exit 1

fi

if [[ ! -e "$источник" ]]; then

echo "Ошибка: источник '$источник' не существует." >&2

exit 2

fi

if ! command -v rsync &> /dev/null; then

echo "Ошибка: требуется rsync, но он не установлен." >&2

exit 3

fi

if [[ ! -d "$назначение" ]]; then

echo "Директория назначения не существует, создаю: $назначение"

mkdir -p "$назначение"

fi

echo "Начинаю резервное копирование '$источник' → '$назначение'..."

if rsync -a "$источник" "$назначение"; then

echo "✓ Резервное копирование успешно завершено."

exit 0

else

echo "✗ Ошибка при выполнении rsync." >&2

exit 4

fi

```

```bash

chmod +x safe_backup.sh

./safe_backup.sh

echo "Код возврата: $?"

```

Что вы увидите: сообщение об использовании и код возврата 1, потому что аргументы не переданы.

```bash

./safe_backup.sh /несуществующий/путь ~/scripts/backup_demo

echo "Код возврата: $?"

```

Код возврата 2 — источник не существует.

```bash

./safe_backup.sh ~/scripts/test_files ~/scripts/backup_target

echo "Код возврата: $?"

ls ~/scripts/backup_target

```

Успешное выполнение, код возврата 0.

[Скриншот всех трёх сценариев]

Шаг 5. Создайте скрипт, демонстрирующий trap для гарантированной очистки временных файлов:

```bash

nano trap_demo.sh

```

Содержимое:

```bash

#!/bin/bash

set -euo pipefail

временный_файл=$(mktemp)

echo "Создан временный файл: $временный_файл"

trap 'echo "Выполняется очистка..."; rm -f "$временный_файл"; echo "Временный файл удалён."' EXIT

echo "Записываю данные во временный файл..."

echo "какие-то важные промежуточные данные" > "$временный_файл"

cat "$временный_файл"

echo "Основная работа скрипта завершена."

```

```bash

chmod +x trap_demo.sh

./trap_demo.sh

```

Что вы увидите: скрипт создаёт временный файл, работает с ним, а при завершении автоматически срабатывает очистка через trap, даже несмотря на то, что мы явно не вызывали rm в конце «основной» логики скрипта.

[Скриншот терминала, демонстрирующий автоматическую очистку]

Шаг 6. Проверьте, что trap срабатывает даже при прерывании скрипта — создайте скрипт с искусственной задержкой:

```bash

nano trap_interrupt.sh

```

Содержимое:

```bash

#!/bin/bash

временный_файл=$(mktemp)

trap 'echo ""; echo "Скрипт прерван, очищаю за собой..."; rm -f "$временный_файл"' EXIT

echo "Временный файл создан: $временный_файл"

echo "Скрипт 'работает' 10 секунд. Нажмите Ctrl+C, чтобы прервать досрочно."

sleep 10

echo "Скрипт завершился естественным путём (эта строка не выведется, если вы прервали через Ctrl+C)."

```

```bash

chmod +x trap_interrupt.sh

./trap_interrupt.sh

```

Нажмите Ctrl+C через пару секунд после запуска. Что вы увидите: несмотря на прерывание, сообщение об очистке всё равно выводится, и временный файл удаляется — trap сработал даже при неожиданном прерывании пользователем.

[Скриншот терминала с прерыванием и последующей очисткой]

Разбор команд

set -e / set -u / set -o pipefail

Команда exit

Команда trap

Команда mktemp

Команда command -v

Возможные ошибки

Почему возникает: set -e останавливает скрипт при любом ненулевом коде возврата, включая случаи, которые логически не являются «ошибками» в контексте конкретного скрипта (например, grep, ничего не нашедший, — это нормальный, ожидаемый результат поиска).

Как исправить: явно обрабатывать такие «ожидаемо неуспешные» команды через if, который сам по себе не прерывает выполнение при set -e (в конструкции if команда; then ... fi, даже если команда возвращает ненулевой код, это не считается «неперехваченной» ошибкой — set -e умеет распознавать это как штатную проверку условия), либо добавлять || true после команды, чтобы явно проигнорировать её код возврата: grep "текст" файл || true.

Почему возникает: сигнал SIGKILL (kill -9, который мы упоминали в Блоке 11) невозможно перехватить никаким trap — это принципиальное архитектурное ограничение операционной системы, гарантирующее, что процесс всегда может быть принудительно завершён, даже если он «сломан» и игнорирует обычные сигналы.

Как исправить/избежать: использовать kill без -9 (обычный SIGTERM) в первую очередь, оставляя -9 только для действительно «зависших» процессов, не реагирующих на вежливое завершение; учитывать, что trap — не абсолютная гарантия очистки в 100% случаев, а защита от «обычных» сценариев прерывания.

Практические задания

  1. Модифицируйте скрипт safe_backup.sh из практики, добавив проверку доступного места на диске в целевой директории до начала копирования (используйте df с соответствующими флагами, как мы делали в предыдущих блоках) — если места меньше определённого порога, скрипт должен вывести предупреждение и завершиться с отдельным кодом ошибки, не пытаясь выполнить копирование.
  2. Напишите скрипт, который создаёт временную директорию через mktemp -d, копирует туда несколько файлов, выполняет с ними какую-то условную «обработку» (например, просто переименование через цикл for), и с помощью trap ... EXIT гарантирует удаление всей временной директории (rm -rf) при завершении скрипта, включая случай прерывания через Ctrl+C.
  3. Найдите (не выполняя, только изучая через man или поиск) дополнительные опции set, которые мы не разбирали подробно (например, set -x для отладочной трассировки выполнения каждой команды) — опишите в конспекте, для какой практической задачи диагностики скриптов может пригодиться set -x.

Проверка знаний

Вопрос 1. Что делает set -e, и почему эта строка считается хорошей практикой почти для любого «серьёзного» bash-скрипта?

Ответ: set -e заставляет скрипт немедленно останавливаться, если любая выполненная в нём команда завершается с ненулевым кодом возврата (кроме случаев, явно проверенных в условии, например if команда). Без set -e скрипт по умолчанию продолжает выполнение после ошибки, что может привести к каскаду дальнейших проблем — например, попытке обработать файл, который на самом деле не был успешно создан на предыдущем шаге из-за незамеченной ошибки. set -e делает такие проблемы видимыми сразу, а не позволяет им незаметно распространяться дальше по скрипту.

Вопрос 2. Для чего используется trap '...' EXIT, и почему это надёжнее, чем просто вызвать команду очистки в конце скрипта как обычную последнюю строку?

Ответ: trap '...' EXIT регистрирует команду, которая гарантированно выполнится при завершении скрипта — независимо от причины завершения: нормальное завершение, вызов exit где-то в середине скрипта, срабатывание set -e из-за ошибки, или прерывание пользователем через Ctrl+C. Если просто разместить команду очистки как последнюю строку скрипта, она выполнится только при штатном, последовательном достижении конца файла — но не сработает, если скрипт завершится досрочно из-за ошибки или будет прерван, что может оставить временные файлы или другие незавершённые ресурсы «висящими» в системе.

Итоги урока

Вы изучили: важность проверки кодов возврата, режимы строгости set -e/set -u/set -o pipefail, команду exit с кодами возврата, механизм trap для гарантированной очистки, команды mktemp и command -v.

Вы умеете: писать надёжные скрипты с явной проверкой ошибок, гарантированно освобождать временные ресурсы даже при неожиданном прерывании, диагностировать типичные проблемы обработки ошибок.

В следующем уроке потребуется: понимание условий и if — теперь мы углубим наши навыки текстовой обработки с помощью регулярных выражений, чтобы делать более сложные и гибкие проверки текста внутри скриптов.

Урок 15.8. Регулярные выражения углублённо

Углубить знания регулярных выражений (введённых кратко в Блоке 6 в контексте grep), освоить более сложные конструкции, понять разницу между basic и extended regex, и научиться применять регулярные выражения непосредственно внутри bash через оператор =~.

Теория

Краткое напоминание. В Блоке 6 мы уже познакомились с регулярными выражениями (regex, regular expressions) — способом описания шаблонов текста, гораздо более гибким, чем простое точное совпадение подстроки. Мы использовали простые примеры с grep. Теперь углубим эти знания и увидим, как регулярные выражения применяются не только с grep, но и напрямую внутри условий bash-скриптов.

Basic vs Extended regex — важное техническое различие. Существует два основных «диалекта» синтаксиса регулярных выражений, исторически сложившихся в Unix-мире:

В этом уроке мы будем использовать преимущественно ERE-синтаксис (grep -E), поскольку он совпадает с синтаксисом, который понимает =~ в Bash, и в целом считается более современным и читаемым стандартом.

Углублённые конструкции регулярных выражений (ERE-синтаксис):

| Конструкция | Значение |

|---|---|

| . | Любой один символ |

| * | Ноль или более повторений предыдущего элемента |

| + | Одно или более повторений предыдущего элемента |

| ? | Ноль или одно повторение (элемент необязателен) |

| ^ | Начало строки |

| $ | Конец строки |

| [абв] | Один символ из перечисленных в скобках (класс символов) |

| [^абв] | Один символ, НЕ входящий в перечисленные (отрицание класса) |

| [0-9] | Один символ из диапазона (в данном случае — любая цифра) |

| [a-zA-Z] | Любая латинская буква, строчная или заглавная |

| {n} | Ровно n повторений предыдущего элемента |

| {n,} | n или более повторений |

| {n,m} | От n до m повторений |

| (...) | Группировка — объединяет несколько символов в один «элемент» для применения квантификаторов, также используется для последующего извлечения совпавшей части |

| \| | Логическое ИЛИ (альтернатива) между вариантами |

Специальные предопределённые классы символов POSIX (более переносимый способ, чем сокращения вроде \d, которые лучше поддерживаются в Perl-совместимых регулярных выражениях, не всегда доступных в стандартном grep):

Примеры практических регулярных выражений:

Оператор =~ внутри [[ ]] — использование regex прямо в условиях Bash.

```bash

if [[ "$строка" =~ ^[0-9]+$ ]]; then

echo "Строка состоит только из цифр"

fi

```

Важные детали:

Захват групп через BASH_REMATCH. Если регулярное выражение содержит группы в скобках (...), после успешного сопоставления через =~ специальный массив BASH_REMATCH содержит найденные совпадения: BASH_REMATCH[0] — вся совпавшая строка целиком, BASH_REMATCH[1] — содержимое первой группы, BASH_REMATCH[2] — второй, и так далее.

```bash

строка="Версия: 5.15.0"

if [[ "$строка" =~ ([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then

echo "Мажорная версия: ${BASH_REMATCH[1]}"

echo "Минорная версия: ${BASH_REMATCH[2]}"

echo "Патч-версия: ${BASH_REMATCH[3]}"

fi

```

Регулярные выражения в grep -E и sed — напоминание и расширение материала Блока 6. Мы уже применяли grep для поиска по шаблону. Теперь с более глубоким пониманием синтаксиса ERE вы можете строить значительно более сложные и точные шаблоны для фильтрации логов, конфигурационных файлов и произвольного текста — это одна из самых часто используемых техник в реальной работе администратора при анализе больших объёмов текстовых данных (например, логов, которые мы будем подробно разбирать в Блоке 18).

Практика

Шаг 1. Потренируйтесь с grep -E на практических примерах, используя углублённый синтаксис:

```bash

cd ~/scripts

echo -e "192.168.1.1\n10.0.0.5\nне IP-адрес\n256.256.256.256\n8.8.8.8" > ip_test.txt

```

Найдите строки, похожие на IPv4-адреса:

```bash

grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' ip_test.txt

```

Что вы увидите: все строки, кроме «не IP-адрес», будут найдены (включая технически некорректный 256.256.256.256 — наше упрощённое выражение не проверяет диапазон значений).

[Скриншот терминала]

Шаг 2. Найдите строки, начинающиеся именно с частных диапазонов (упрощённо):

```bash

grep -E '^(192\.168|10\.)' ip_test.txt

```

Что вы увидите: только 192.168.1.1 и 10.0.0.5.

Шаг 3. Создайте скрипт, использующий =~ для валидации ввода:

```bash

nano validate_email.sh

```

Содержимое:

```bash

#!/bin/bash

email="${1:-}"

шаблон='^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$'

if [[ -z "$email" ]]; then

echo "Использование: $0 email@example.com"

exit 1

fi

if [[ "$email" =~ $шаблон ]]; then

echo "'$email' выглядит как корректный email-адрес."

else

echo "'$email' НЕ похож на корректный email-адрес."

fi

```

```bash

chmod +x validate_email.sh

./validate_email.sh "user@example.com"

./validate_email.sh "не email вообще"

./validate_email.sh "user@sub.example.co.uk"

```

[Скриншот терминала со всеми результатами]

Шаг 4. Создайте скрипт, демонстрирующий захват групп через BASH_REMATCH — извлечение версии ядра:

```bash

nano parse_kernel_version.sh

```

Содержимое:

```bash

#!/bin/bash

версия_ядра=$(uname -r)

echo "Полная строка версии ядра: $версия_ядра"

if [[ "$версия_ядра" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then

echo "Мажорная версия: ${BASH_REMATCH[1]}"

echo "Минорная версия: ${BASH_REMATCH[2]}"

echo "Патч-версия: ${BASH_REMATCH[3]}"

else

echo "Не удалось разобрать версию по ожидаемому шаблону."

fi

```

```bash

chmod +x parse_kernel_version.sh

./parse_kernel_version.sh

```

[Скриншот с извлечёнными компонентами версии]

Шаг 5. Примените regex для практической задачи — анализ лога SSH на предмет неудачных попыток входа (создадим тестовый лог для практики, поскольку реальный системный лог требует прав root и может не содержать нужных строк прямо сейчас):

```bash

cat > fake_auth.log << 'EOF'

Jul 22 10:15:03 server sshd[1234]: Failed password for admin from 203.0.113.50 port 54321 ssh2

Jul 22 10:15:10 server sshd[1234]: Failed password for root from 203.0.113.50 port 54322 ssh2

Jul 22 10:16:00 server sshd[1235]: Accepted publickey for anna from 198.51.100.10 port 22334 ssh2

Jul 22 10:17:45 server sshd[1236]: Failed password for invalid user test from 192.0.2.77 port 33445 ssh2

EOF

```

Извлеките все IP-адреса, связанные с неудачными попытками:

```bash

grep -E "Failed password" fake_auth.log | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'

```

(Флаг -o у grep, который мы не разбирали подробно ранее — выводит только совпавшую часть строки, а не строку целиком, что чрезвычайно полезно именно для извлечения данных из текста.)

Что вы увидите: список IP-адресов, встретившихся в строках с неудачными попытками входа — 203.0.113.50 (дважды) и 192.0.2.77.

[Скриншот результата]

Шаг 6. Посчитайте количество неудачных попыток по каждому уникальному IP, комбинируя уже знакомые нам из Блока 6 команды с regex:

```bash

grep -E "Failed password" fake_auth.log | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | sort | uniq -c | sort -rn

```

Что вы увидите: отсортированный по убыванию список — какой IP-адрес сколько раз встретился среди неудачных попыток. Это практически ровно та логика, которую использует Fail2Ban (изученный в Блоке 14) «под капотом» для принятия решения о блокировке.

[Скриншот итогового анализа]

Разбор команд

Оператор =~

Массив BASH_REMATCH

Флаг -o у grep

Возможные ошибки

Почему возникает: обычный grep использует BRE (Basic Regular Expressions), где эти символы нужно экранировать \, чтобы они получили специальное значение — без экранирования они трактуются буквально.

Как исправить: использовать grep -E (ERE) для более интуитивного синтаксиса без необходимости экранирования, либо явно экранировать нужные символы (\+, \?, \|) при использовании обычного grep.

Почему возникает: в некоторых версиях/режимах Bash кавычки справа от =~ заставляют интерпретировать содержимое как обычную строку для точного сравнения, а не как regex-шаблон.

Как исправить/избежать: не заключать регулярное выражение в кавычки при использовании напрямую справа от =~ (как в примерах практики этого урока), либо (более надёжный вариант, применённый в шаге 3 практики) сохранить шаблон в отдельную переменную без кавычек вокруг regex-специальных символов и подставлять переменную без кавычек.

Практические задания

  1. Напишите регулярное выражение и протестируйте его через grep -E, которое находит строки, содержащие корректный номер телефона в формате +371-XXXXXXXX (код страны Латвии, плюс восемь цифр после дефиса) — создайте тестовый файл с несколькими похожими и непохожими строками для проверки.
  2. Используя навыки этого урока и материал Блока 14 (Fail2Ban), напишите скрипт analyze_auth_log.sh, который принимает путь к лог-файлу как аргумент, находит все IP-адреса, связанные со строками «Failed password», и выводит топ-3 самых часто встречающихся IP (используйте комбинацию grep/sort/uniq, как в шаге 6 практики, но добавьте head -3 для ограничения вывода).
  3. Напишите функцию является_валидным_hostname(), использующую =~ для проверки, что переданная строка соответствует упрощённым правилам доменного имени (буквы, цифры, дефисы, точки — например, ^[a-zA-Z0-9.-]+$), и продемонстрируйте её работу на нескольких примерах через if.

Проверка знаний

Вопрос 1. В чём разница между BRE (Basic Regular Expressions) и ERE (Extended Regular Expressions) с практической точки зрения при написании регулярного выражения?

Ответ: В BRE (используется обычным grep без флагов) специальные символы, такие как +, ?, |, (, ), нужно экранировать обратным слэшем (\+, \? и т.д.), чтобы они получили своё специальное значение — без экранирования они означают буквальные символы. В ERE (используется grep -E и оператором =~ в Bash) эти же символы работают как специальные операторы без экранирования, что делает синтаксис интуитивнее и ближе к тому, что можно встретить в других языках программирования, поддерживающих регулярные выражения.

Вопрос 2. Что содержит массив BASH_REMATCH после успешного применения оператора =~ с регулярным выражением, содержащим группы в скобках?

Ответ: BASH_REMATCH[0] содержит всю часть строки, совпавшую с регулярным выражением целиком. BASH_REMATCH[1], BASH_REMATCH[2] и так далее содержат части строки, совпавшие с соответствующими группами в скобках (...), пронумерованными по порядку появления открывающей скобки в самом регулярном выражении. Это позволяет не только проверить соответствие шаблону, но и извлечь конкретные значимые части найденной строки для дальнейшего использования в скрипте.

Итоги урока

Вы изучили: различие BRE/ERE, углублённые конструкции регулярных выражений (квантификаторы {n,m}, классы символов, POSIX-классы), оператор =~ для проверки regex прямо в условиях Bash, массив BASH_REMATCH для извлечения групп, флаг -o у grep.

Вы умеете: писать и тестировать сложные регулярные выражения, применять их и в grep, и напрямую в bash-условиях, извлекать структурированные данные из текста с помощью групп захвата.

В последнем уроке этого блока потребуется: всё, что мы изучили о скриптах — теперь мы научимся запускать их автоматически, по расписанию, без ручного вмешательства, с помощью cron.

Урок 15.9. Автоматизация по расписанию: `cron` и свои скрипты

Освоить систему cron — стандартный механизм Linux для запуска задач по расписанию, понять синтаксис crontab-выражений, научиться настраивать регулярный автоматический запуск собственных скриптов.

Теория

Зачем нужна автоматизация по расписанию. Мы написали множество полезных скриптов в этом блоке: резервное копирование, проверка безопасности, анализ логов. Но их практическая ценность резко возрастает, если они запускаются автоматически, без необходимости помнить о них и запускать вручную каждый день. Именно для этого существует cron — классический и повсеместно используемый в Unix-мире механизм планирования задач.

Что такое cron. cron — это фоновая служба (демон — мы подробно разбирали демоны и службы в Блоке 10), которая постоянно работает в системе и периодически (обычно каждую минуту) проверяет список запланированных задач, запуская те, время выполнения которых наступило. Список задач для конкретного пользователя хранится в специальном файле, называемом crontab (сокращение от «cron table»).

Файл crontab и его редактирование. У каждого пользователя системы может быть собственный crontab. Редактировать его напрямую как обычный файл не рекомендуется — вместо этого используется специальная команда:

```bash

crontab -e

```

Она открывает crontab текущего пользователя в редакторе по умолчанию (обычно nano, если не настроено иначе) для редактирования, и при сохранении автоматически проверяет синтаксис и активирует изменения — сам cron-демон не нужно перезапускать вручную, он сам подхватывает изменения.

Другие полезные команды:

Синтаксис записи в crontab — пять полей времени плюс команда:

```

│ │ │ │ │

│ │ │ │ └─── день недели (0-7, где и 0, и 7 означают воскресенье)

│ │ │ └───── месяц (1-12)

│ │ └─────── день месяца (1-31)

│ └───────── час (0-23)

└─────────── минута (0-59)

```

Символ * (звёздочка) на месте любого поля означает «каждое значение» — то есть «в любую минуту», «в любой час» и так далее, в зависимости от позиции.

Примеры записей crontab:

```

30 3 * /home/user/scripts/backup.sh

0 /home/user/scripts/check_status.sh

/15 * /home/user/scripts/monitor.sh

0 9 1 /home/user/scripts/weekly_report.sh

0 0 1 /home/user/scripts/monthly_cleanup.sh

0 18 1-5 /home/user/scripts/end_of_day.sh

```

Дополнительный синтаксис в полях времени:

Специальные символические сокращения (поддерживаются во многих реализациях cron, включая используемую в Ubuntu, как альтернатива пяти полям):

```

@reboot команда # запустить один раз при каждой перезагрузке системы

@daily команда # эквивалент "0 0 *" — раз в день, в полночь

@weekly команда # эквивалент "0 0 0" — раз в неделю

@monthly команда # эквивалент "0 0 1 " — раз в месяц

@hourly команда # эквивалент "0 " — раз в час

```

Критически важный практический нюанс: окружение cron-заданий отличается от вашего интерактивного терминала. Задания cron выполняются в существенно более «бедном» окружении, чем ваша обычная интерактивная сессия терминала — многие переменные окружения (включая часть $PATH), которые вы привыкли иметь доступными, могут отсутствовать. Это одна из самых частых причин, по которым «скрипт прекрасно работает, когда я его запускаю вручную, но почему-то не работает через cron». Практические рекомендации, чтобы избежать этой проблемы:

  1. Всегда используйте полные, абсолютные пути — как к самому скрипту в записи crontab, так и внутри скрипта ко всем используемым файлам и, желательно, даже к вызываемым командам (или хотя бы быть уверенным, что нужные команды находятся в базовом $PATH, который есть у cron).
  2. Перенаправляйте вывод в лог-файл, чтобы иметь возможность увидеть, что произошло (или пошло не так) при автоматическом запуске, поскольку обычный вывод cron-заданий никуда не отображается интерактивно — он либо отправляется по электронной почте локальному пользователю (если настроена почтовая система, что не всегда так на простом сервере), либо, если не перенаправлен явно, может быть просто потерян:

```

30 3 * /home/user/scripts/backup.sh >> /home/user/scripts/backup.log 2>&1

```

(Мы уже разбирали >> для добавления в конец файла и 2>&1 для объединения потоков ошибок и обычного вывода в Блоке 6 — здесь это особенно важно, чтобы не потерять диагностическую информацию о работе автоматической задачи.)

Системный crontab и /etc/cron.d/. Помимо пользовательских crontab (редактируемых через crontab -e), существуют и системные способы настройки заданий — файл /etc/crontab и директория /etc/cron.d/ (для заданий, устанавливаемых пакетами программ), а также уже знакомые нам по уроку 15.1 директории /etc/cron.daily/, /etc/cron.weekly/, /etc/cron.monthly/ — куда достаточно просто положить исполняемый скрипт (без специального crontab-синтаксиса), чтобы он автоматически запускался с соответствующей периодичностью. Для наших личных скриптов, однако, использование crontab -e (пользовательский crontab) — наиболее подходящий и распространённый вариант.

Практика

Шаг 1. Просмотрите текущий (скорее всего, пустой) crontab:

```bash

crontab -l

```

Что вы увидите: скорее всего, сообщение no crontab for ваш_пользователь — задач пока не запланировано.

[Скриншот терминала]

Шаг 2. Создайте простой скрипт для тестирования cron — он будет каждый раз дописывать метку времени в файл, что позволит нам наглядно убедиться, что cron действительно запускает его автоматически:

```bash

cd ~/scripts

nano cron_test.sh

```

Содержимое:

```bash

#!/bin/bash

echo "Скрипт запущен: $(date '+%Y-%m-%d %H:%M:%S')" >> "$HOME/scripts/cron_test.log"

```

```bash

chmod +x cron_test.sh

```

Шаг 3. Откройте crontab для редактирования:

```bash

crontab -e

```

Если это первый запуск — система может спросить, какой редактор использовать по умолчанию; выберите nano (обычно вариант 1).

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

```

```

(Замените ваш_пользователь на реальное имя, или используйте $HOME — но учтите, что в crontab переменные окружения могут работать не так, как в интерактивной оболочке, поэтому в этом конкретном случае надёжнее явно прописать полный путь.)

Сохраните: Ctrl+O, Enter, Ctrl+X.

[Скриншот редактирования crontab]

Шаг 5. Убедитесь, что задание добавлено:

```bash

crontab -l

```

Шаг 6. Подождите 2-3 минуты (cron проверяет расписание раз в минуту), затем проверьте лог:

```bash

cat ~/scripts/cron_test.log

```

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

[Скриншот содержимого лог-файла с несколькими записями]

Шаг 7. Удалите тестовое задание (каждую минуту — это только для учебной демонстрации, в реальной практике так делать не стоит):

```bash

crontab -e

```

Удалите добавленную строку (выделите и удалите её в nano), сохраните.

Шаг 8. Настройте более реалистичную задачу — ежедневный запуск скрипта резервного копирования из мини-проекта Блока 13, если он у вас ещё сохранился (или используйте security_check.sh из Блока 14):

```bash

crontab -e

```

Добавьте:

```

0 2 * /home/ваш_пользователь/scripts/security_check.sh >> /home/ваш_пользователь/scripts/security_check_cron.log 2>&1

```

Это запустит проверку безопасности каждый день в 2 часа ночи, сохраняя весь вывод (и обычный, и ошибки) в лог-файл.

Сохраните и проверьте:

```bash

crontab -l

```

[Скриншот финальной настройки crontab]

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

```bash

ls -la /etc/cron.daily/

ls -la /etc/cron.weekly/

```

Шаг 10. Проверьте, что сама служба cron работает (используя знакомые нам из Блока 10 команды):

```bash

sudo systemctl status cron

```

Что вы увидите: Active: active (running) — служба cron работает в фоне, обеспечивая выполнение всех запланированных заданий.

[Скриншот статуса службы cron]

Разбор команд

Команда crontab

Формат записи crontab — пять полей времени + команда:

```

минута(0-59) час(0-23) день_месяца(1-31) месяц(1-12) день_недели(0-7) команда

```

Возможные ошибки

Почему возникает: cron выполняет задания в существенно урезанном окружении по сравнению с интерактивной оболочкой — переменная $PATH может не включать все директории, которые есть у вас в интерактивном терминале, из-за чего некоторые вызываемые внутри скрипта команды могут не находиться.

Как определить: перенаправить вывод в лог-файл (как в шаге 8 практики) и изучить сообщения об ошибках — часто там будет command not found для команды, которая прекрасно работает при ручном запуске.

Как исправить: использовать полные абсолютные пути к командам внутри скрипта (например, /usr/bin/rsync вместо просто rsync), либо явно указать нужный $PATH в начале самого скрипта: export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin.

Как избежать: всегда тестировать реальное поведение через cron (а не только ручной запуск скрипта), особенно для критически важных автоматических задач, и всегда перенаправлять вывод в лог для последующей диагностики.

Почему возникает: невнимательность при очистке после экспериментов (как в практике этого урока).

Как определить: периодически проверять crontab -l на предмет забытых или устаревших заданий.

Как исправить/избежать: взять за правило регулярно просматривать содержимое crontab -l (аналогично общей практике аудита из урока 14.6), особенно после учебных экспериментов.

Практические задания

  1. Настройте задание cron, которое каждый будний день (понедельник–пятница) в 8:00 утра выполняет ваш скрипт system_info.sh из практических заданий урока 15.2, перенаправляя вывод в лог-файл с меткой даты в имени.
  2. Изучите синтаксис */N подробнее — настройте (для тестирования, затем удалите) задание, запускающееся каждые 5 минут, и через 15–20 минут проверьте лог, чтобы убедиться, что интервал соблюдается точно как ожидалось.
  3. Найдите (без выполнения) информацию о переменной окружения MAILTO, которую можно установить в начале файла crontab (MAILTO="ваш@email.com") — опишите в конспекте, для чего она может быть полезна в контексте автоматизированных задач на реальном рабочем сервере.

Проверка знаний

Вопрос 1. Что означает запись /15 8-18 * 1-5 в первых пяти полях crontab, и в какие моменты времени она будет запускать задачу?

Ответ: Эта запись означает: каждые 15 минут (/15 в поле минут), в часы с 8 до 18 включительно (8-18 в поле часов), в любой день месяца (), в любой месяц (*), но только по будним дням — с понедельника по пятницу (1-5 в поле дня недели). То есть задача будет запускаться каждые 15 минут (в 0, 15, 30, 45 минут каждого часа) с 8:00 до 18:45, но только в рабочие дни недели.

Вопрос 2. Почему важно перенаправлять вывод cron-заданий в лог-файл (>> файл.log 2>&1), а не полагаться на то, что вывод будет виден так же, как при ручном запуске в терминале?

Ответ: Cron-задания выполняются в фоне, без привязки к интерактивному терминалу — их вывод по умолчанию либо теряется, либо (если настроена система почты на сервере) отправляется по электронной почте локальному пользователю, что не всегда удобно или вообще работает на простом сервере. Перенаправление вывода (и обычного, и потока ошибок через 2>&1) в конкретный лог-файл гарантирует, что вся диагностическая информация о работе автоматической задачи сохраняется и доступна для последующего изучения — это особенно важно, учитывая, что окружение cron отличается от интерактивного и задания могут завершаться с ошибками, незаметными без явного логирования.

Итоги урока

Вы изучили: принцип работы cron и crontab, синтаксис пяти полей времени, дополнительные конструкции (*/N, диапазоны, списки), символические сокращения (@daily и подобные), критический нюанс отличающегося окружения cron-заданий, системные директории периодических задач.

Вы умеете: создавать, просматривать и удалять записи в персональном crontab, настраивать автоматический запуск собственных скриптов по расписанию, диагностировать и предотвращать типичные проблемы с окружением cron.

В следующем блоке потребуется: весь материал этого блока (переменные, условия, циклы, функции, обработка ошибок, regex, cron) — Блок 16 про Git не требует напрямую bash-скриптов, но многие реальные рабочие процессы разработки объединяют Git-операции именно через bash-скрипты автоматизации.

Мини-проект блока

Задача: создать полноценную, надёжную систему из нескольких взаимодействующих bash-скриптов, объединяющую весь материал блока — переменные, условия, циклы, функции, обработку ошибок, регулярные выражения и планирование через cron.

Что нужно сделать:

  1. Создайте скрипт backup_system.sh, использующий:
  1. Создайте скрипт log_analyzer.sh, использующий:
  1. Создайте скрипт daily_maintenance.sh, объединяющий вызовы обоих предыдущих скриптов:
  1. Настройте cron, чтобы daily_maintenance.sh запускался автоматически раз в день в удобное время, с перенаправлением всего вывода в общий лог-файл.
  1. Протестируйте всю систему целиком: временно измените расписание cron на «каждую минуту» для быстрой проверки (как мы делали в практике урока 15.9), убедитесь, что все части работают корректно вместе, изучите лог, затем верните расписание на реалистичное («раз в день») и уберите тестовую версию задания.

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

Контрольные вопросы

  1. Объясните назначение shebang #!/bin/bash и опишите, что произойдёт при попытке выполнить ./script.sh, если эта строка отсутствует или содержит опечатку в пути.
  2. В чём разница между $@ и $* при использовании внутри двойных кавычек, если один из переданных аргументов содержит пробел?
  3. Напишите (на бумаге или в текстовом виде, без выполнения) пример конструкции if/elif/else, проверяющей, является ли переданное первым аргументом число ($1) положительным, отрицательным или нулём, используя [[ ]].
  4. Чем цикл until отличается от while по своей базовой логике?
  5. Объясните разницу между использованием return и echo для «возврата» результата из bash-функции — когда уместен каждый из этих подходов?
  6. Что делает set -euo pipefail, и почему эта комбинация считается хорошей практикой в начале серьёзных bash-скриптов?
  7. Приведите пример регулярного выражения (ERE-синтаксис) для проверки, что строка состоит только из заглавных латинских букв и цифр, и не короче трёх символов.
  8. Опишите синтаксис пяти полей времени в записи crontab и приведите пример записи, запускающей скрипт каждые 10 минут только по выходным (суббота и воскресенье).
  9. Почему скрипты, прекрасно работающие при ручном запуске в терминале, иногда работают некорректно при запуске через cron, и как этого избежать?

Выводы по блоку

Это один из самых объёмных и насыщенных блоков курса — и не случайно: bash-скрипты — это тот навык, который превращает разрозненные знания отдельных команд Linux (накопленные в Блоках 1–14) в единую, мощную способность автоматизировать практически любую повторяющуюся задачу администрирования. Вы прошли путь от простейшего скрипта с несколькими командами до полноценных, надёжных программ с обработкой аргументов, условной логикой, циклами, переиспользуемыми функциями, продуманной обработкой ошибок, продвинутой текстовой обработкой через регулярные выражения и автоматическим запуском по расписанию.

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

Материал этого блока будет активно использоваться до самого конца курса: в Блоке 16 (Git) скрипты часто автоматизируют операции с репозиториями; в Блоке 17 (Docker) — процессы сборки и развёртывания контейнеров; в Блоке 18 (мониторинг) — сбор и анализ метрик; в Блоке 19 (веб-серверы и БД) — автоматизация развёртывания приложений; в Блоке 20 (серверное администрирование) — практически весь процесс настройки и обслуживания сервера; и, конечно, в финальном практическом проекте Блока 21.

Список изученных тем

Рекомендации перед переходом

Убедитесь, что вы умеете:

В Блоке 16 мы сменим тему — займёмся Git, системой контроля версий, без которой не обходится практически ни один современный проект разработки программного обеспечения. Хотя Git — это отдельная большая тема, вы обнаружите, что многие рабочие процессы (автоматическое резервное копирование репозиториев, хуки автоматизации, скрипты развёртывания через Git) прямо связаны с только что изученными bash-скриптами.