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

Как это работает
Скрипт это текстовый файл. Чтобы ядро знало, кем его запускать, в первой строке ставят shebang - два символа #! и путь к интерпретатору. Когда вы запускаете файл как программу, ядро читает эти первые байты, видит #!/bin/bash и фактически вызывает /bin/bash ваш_файл. Без shebang файл выполнит текущая оболочка, что не всегда bash - в Debian/Ubuntu /bin/sh это dash, и многие конструкции bash там просто не работают. Поэтому для bash-специфики пишите явно #!/bin/bash, а для переносимого POSIX-скрипта #!/bin/sh.
Чтобы файл можно было запустить как ./script.sh, ему нужен бит исполнения (chmod +x). Без него ядро откажет с Permission denied, хотя bash script.sh всё равно сработает - тут вы запускаете интерпретатор явно, а файл для него просто данные.
Условия в bash строятся вокруг кода возврата. Каждая команда после завершения отдаёт число 0-255: ноль значит успех, всё остальное - ошибка. Это число лежит в переменной $?. Конструкция if смотрит именно на код возврата команды, а не на текст. Команда test (она же [ ]) сама ничего не печатает - она лишь возвращает 0 или 1 в зависимости от того, истинно условие или нет. Современная замена для bash - [[ ]]: это ключевое слово оболочки, а не команда, поэтому внутри не нужно экранировать < и >, безопаснее с пустыми переменными, и оно умеет && || и сравнение по шаблону =~.
Циклы дают повтор: for идёт по списку слов, while крутится пока команда успешна, until - пока неуспешна. Аргументы, переданные скрипту, лежат в позиционных параметрах $1 $2 и так далее, $# хранит их количество, $@ - все сразу. exit завершает скрипт и задаёт его собственный код возврата, который увидит вызывающий.
Команды и примеры
Скелет скрипта и запуск:
Код: Выделить всё
#!/bin/bash
set -euo pipefail # строгий режим: падать на ошибке, на необъявленной переменной, ловить сбой в пайпе
echo "Привет, $USER"Код: Выделить всё
chmod +x backup.sh # дать право на исполнение
./backup.sh # запуск как программы (нужен shebang и +x)
bash backup.sh # запуск явным интерпретатором (+x не нужен)Код: Выделить всё
if [ "$1" = "start" ]; then
echo "запускаю"
elif [ -z "$1" ]; then
echo "нет аргумента"
else
echo "неизвестно: $1"
fiКод: Выделить всё
if [[ -f /etc/os-release && -r /etc/os-release ]]; then
echo "файл есть и читается"
fi
# сравнение чисел: -eq -ne -lt -le -gt -ge
n=7
if [[ $n -gt 3 && $n -lt 10 ]]; then echo "в диапазоне"; fi
# шаблон и регэксп
host=web01
if [[ $host == web* ]]; then echo "веб-сервер"; fi
if [[ $host =~ ^web[0-9]+$ ]]; then echo "имя по маске"; ficase удобнее длинной лесенки elif - выбор по шаблону:
Код: Выделить всё
case "$1" in
start) echo "старт" ;;
stop|restart) echo "стоп или рестарт" ;;
*.conf) echo "конфиг" ;;
*) echo "по умолчанию" ;;
esacКод: Выделить всё
for f in /var/log/*.log; do
echo "обрабатываю $f"
done
for i in $(seq 1 5); do echo "шаг $i"; done # seq печатает 1 2 3 4 5
for i in {1..5}; do echo "шаг $i"; done # то же без внешней команды
n=0
while [[ $n -lt 3 ]]; do
echo "while $n"; ((n++))
done
until ping -c1 -W1 8.8.8.8 &>/dev/null; do
echo "сети нет, жду..."; sleep 2
doneКод: Выделить всё
while IFS= read -r line; do
echo ">> $line"
done < /etc/hostnameКод: Выделить всё
#!/bin/bash
echo "имя скрипта: $0"
echo "первый аргумент: $1"
echo "всего аргументов: $#"
echo "все аргументы: $@"
today=$(date +%F) # подстановка: вывод команды попадает в переменную
echo "сегодня $today"
if ! systemctl is-active --quiet nginx; then
echo "nginx не работает" >&2 # ошибки - в stderr
exit 1 # свой код возврата
fi
echo "проверка пройдена"
exit 0Код: Выделить всё
./check.sh nginx
echo $? # код возврата последней команды: 0 успех, не 0 ошибкаКод: Выделить всё
read -rp "Удалить файлы? [y/N] " ans
[[ $ans == [yY] ]] && echo "удаляю"
# базовая отладка: видеть каждую команду после раскрытия
set -x
cp file.txt /tmp/
set +x # выключить трассировкуКод: Выделить всё
if command -v apt >/dev/null; then
apt install -y htop # Debian 13 / Ubuntu 24.04
elif command -v dnf >/dev/null; then
dnf install -y htop # RHEL 10 / Fedora 41+ (dnf5)
fi- Пробелы вокруг = в test обязательны. [ "$x"="y" ] всегда истинно, потому что это проверка одной непустой строки, а не сравнение. Нужно [ "$x" = "y" ].
- Непроставленные кавычки. Если $f пустая или содержит пробел, [ -f $f ] ломается с ошибкой синтаксиса. Либо кавычьте, либо используйте [[ ]], где это не страшно.
- = в test это строки, -eq это числа. [ "08" = "8" ] ложно, [ "08" -eq "8" ] истинно. Путаница даёт тихие баги.
- for line in $(cat file) рвёт строки по пробелам, а не по переводам строки. Читайте файлы через while read.
- Забыли chmod +x - ./script.sh даёт Permission denied. Либо chmod, либо запуск через bash script.sh.
- #!/bin/sh не равно bash. В Debian/Ubuntu sh это dash, и [[ ]], массивы, ((...)) там не работают. Для bash-фич явно ставьте #!/bin/bash.
- set -e не панацея: команда в if или с || не считается фатальной, и пайп без pipefail прячет ошибку левой части.
- read без -r съедает обратные слэши. Почти всегда нужен read -r.
- Создайте файл svccheck.sh, первой строкой #!/bin/bash, второй set -euo pipefail.
- Сделайте его исполняемым через chmod +x и запустите без аргументов - добейтесь, чтобы он печатал в stderr подсказку об использовании и выходил с exit 2, если $# равно нулю.
- Принимайте имя сервиса как $1 и через systemctl is-active --quiet проверяйте его состояние, ветвление сделайте на if/else.
- Через case по $1 обработайте особый случай: если передали слово all - в цикле for пройдите по списку sshd cron и проверьте каждый.
- Добавьте подстановку команд: в вывод включите дату через $(date +%T) и имя хоста через $(hostname).
- Включите set -x в начале, прогоните, посмотрите трассировку, затем уберите и сравните вывод.
- Проверьте echo $? после успешного и после неуспешного запуска - убедитесь, что коды разные.
- Что делает ядро, встретив строку #!/bin/bash, и почему скрипт без shebang может вести себя по-разному в разных системах?
- Чем [[ ]] отличается от [ ] и от команды test? Назовите минимум два практических преимущества [[ ]].
- В чём разница между $@, $# и $0? Что вернёт $? и какие значения считаются успехом?
- Почему чтение файла построчно делают через while IFS= read -r, а не через for line in $(cat file)?
- Когда выбрать case вместо цепочки if/elif, и как в case задать несколько шаблонов на одну ветку?
- Что делает set -x и чем он отличается от set -e и set -o pipefail?