8.1 Что такое скрипт
Скрипт — это текстовый файл, содержащий последовательность команд, которые выполняются интерпретатором (shell).
Преимущества скриптов
Автоматизация
Выполнение повторяющихся задач без ручного ввода команд
Экономия времени
Сложные операции выполняются одной командой
Надежность
Исключение человеческих ошибок при выполнении задач
Shebang (#!/bin/bash)
Первая строка скрипта, которая указывает интерпретатор для выполнения скрипта.
#!/bin/bash
# Это комментарий
echo "Hello World"
Типы скриптов
Системные скрипты
- Автозапуск сервисов
- Резервное копирование
- Мониторинг системы
Пользовательские скрипты
- Автоматизация рутинных задач
- Обработка файлов
- Развертывание приложений
•
#!/bin/bash — Bash (самый популярный)•
#!/bin/sh — POSIX shell•
#!/usr/bin/python3 — Python•
#!/usr/bin/perl — Perl
8.2 Создание и запуск скриптов
Создание и запуск bash-скриптов — основа автоматизации в Linux.
Создание скрипта
nano hello.sh # создать скрипт в nano
# Или
vim hello.sh # создать скрипт в vim
Содержимое скрипта
#!/bin/bash
# Мой первый скрипт
echo "Привет, мир!"
echo "Текущая дата: $(date)"
echo "Текущий пользователь: $USER"
Сделать скрипт исполняемым
chmod +x hello.sh # добавить права на выполнение
ls -l hello.sh # проверить права
# -rwxr-xr-x 1 user group 123 Dec 15 10:30 hello.sh
Запуск скрипта
./hello.sh # запустить скрипт
# Или
bash hello.sh # запустить через bash
Проверка синтаксиса
bash -n script.sh # проверить синтаксис без выполнения
bash -x script.sh # выполнить с выводом команд
Отладка скрипта
# Добавить в скрипт для отладки:
set -x # включить отладку
echo "Отладочная информация"
set +x # выключить отладку
8.3 Переменные в bash
Переменные в bash позволяют хранить и использовать данные в скриптах.
Создание переменных
name="Иван" # создать переменную
age=25 # числовая переменная
echo "Привет, $name!" # использовать переменную
echo "Вам $age лет" # использовать переменную
Системные переменные
echo $HOME # домашний каталог
echo $USER # имя пользователя
echo $PWD # текущий каталог
echo $PATH # пути поиска команд
echo $0 # имя скрипта
Переменные окружения
export MY_VAR="значение" # создать переменную окружения
echo $MY_VAR # использовать переменную
env | grep MY_VAR # показать переменную окружения
Специальные переменные
echo $# # количество аргументов
echo $1 # первый аргумент
echo $2 # второй аргумент
echo $@ # все аргументы
echo $? # код возврата последней команды
Работа с переменными
# Проверка существования переменной
if [ -z "$MY_VAR" ]; then
echo "Переменная не задана"
fi
# Значение по умолчанию
echo ${MY_VAR:-"по умолчанию"}
# Длина строки
text="Hello"
echo ${#text} # выведет 5
8.4 Ввод данных
Команда read позволяет получать данные от пользователя во время выполнения скрипта.
Базовое использование read
#!/bin/bash
echo "Введите ваше имя:"
read name
echo "Привет, $name!"
Ввод с приглашением
read -p "Введите ваш возраст: " age
echo "Вам $age лет"
Скрытый ввод (для паролей)
read -s -p "Введите пароль: " password
echo "Пароль введен"
Ввод нескольких значений
read -p "Введите имя и возраст: " name age
echo "Имя: $name, Возраст: $age"
Ввод с таймаутом
if read -t 5 -p "Введите что-то за 5 секунд: " input; then
echo "Вы ввели: $input"
else
echo "Время истекло"
fi
Чтение из файла
# Читать файл построчно
while read line; do
echo "Строка: $line"
done < file.txt
Практический пример
#!/bin/bash
echo "=== Создание пользователя ==="
read -p "Имя пользователя: " username
read -s -p "Пароль: " password
echo
read -p "Полное имя: " fullname
echo "Создаем пользователя $username..."
# Здесь была бы команда создания пользователя
echo "Пользователь $username создан"
8.5 Подстановка команд
Подстановка команд позволяет использовать результат выполнения команды как значение переменной или аргумента.
Синтаксис подстановки
# Старый синтаксис (обратные кавычки)
current_date=`date`
echo "Сегодня: $current_date"
# Новый синтаксис (рекомендуется)
current_date=$(date)
echo "Сегодня: $current_date"
Использование в переменных
# Получить имя хоста
hostname=$(hostname)
echo "Имя компьютера: $hostname"
# Получить количество файлов
file_count=$(ls -1 | wc -l)
echo "Файлов в каталоге: $file_count"
# Получить размер каталога
dir_size=$(du -sh . | cut -f1)
echo "Размер каталога: $dir_size"
Использование в командах
# Создать резервную копию с датой
cp important.txt backup_$(date +%Y%m%d).txt
# Показать файлы, измененные сегодня
ls -la $(find . -mtime 0)
# Подсчитать строки в файлах
echo "Всего строк: $(cat *.txt | wc -l)"
Вложенные подстановки
# Получить имя каталога из полного пути
current_dir=$(basename $(pwd))
echo "Текущий каталог: $current_dir"
# Получить количество процессов
process_count=$(ps aux | wc -l)
echo "Процессов запущено: $process_count"
Практические примеры
#!/bin/bash
# Скрипт для создания резервной копии
backup_dir="backup_$(date +%Y%m%d_%H%M%S)"
mkdir "$backup_dir"
echo "Создаем резервную копию в $backup_dir"
cp -r /home/user/documents "$backup_dir/"
echo "Резервная копия создана"
echo "Размер: $(du -sh "$backup_dir" | cut -f1)"
$() вместо обратных кавычек, так как это более читаемо и поддерживает вложенность.
8.6 Математические операции
В bash есть несколько способов выполнения математических операций.
Арифметическое расширение $(( ))
a=10
b=5
result=$((a + b))
echo "Сумма: $result"
# Различные операции
echo "Сложение: $((a + b))" # 15
echo "Вычитание: $((a - b))" # 5
echo "Умножение: $((a * b))" # 50
echo "Деление: $((a / b))" # 2
echo "Остаток: $((a % b))" # 0
Команда expr
a=10
b=5
result=$(expr $a + $b)
echo "Результат: $result"
# Обратите внимание на пробелы вокруг операторов
echo "Умножение: $(expr $a \* $b)" # экранирование *
Команда bc (калькулятор)
# Простые вычисления
echo "2 + 3" | bc # 5
echo "10 / 3" | bc # 3
echo "scale=2; 10 / 3" | bc # 3.33 (2 знака после запятой)
# Сложные вычисления
echo "sqrt(16)" | bc # 4
echo "2^3" | bc # 8
Инкремент и декремент
counter=0
echo "Начальное значение: $counter"
counter=$((counter + 1))
echo "После увеличения: $counter"
counter=$((counter - 1))
echo "После уменьшения: $counter"
# Сокращенная запись
counter=$((counter++)) # постфиксный инкремент
counter=$((++counter)) # префиксный инкремент
Сравнения
a=10
b=5
if [ $a -gt $b ]; then
echo "$a больше $b"
fi
if [ $a -eq 10 ]; then
echo "$a равно 10"
fi
Практический пример
#!/bin/bash
# Калькулятор
read -p "Введите первое число: " num1
read -p "Введите второе число: " num2
read -p "Введите операцию (+, -, *, /): " op
case $op in
+) result=$((num1 + num2)) ;;
-) result=$((num1 - num2)) ;;
\*) result=$((num1 * num2)) ;;
/) result=$((num1 / num2)) ;;
*) echo "Неизвестная операция"; exit 1 ;;
esac
echo "Результат: $result"
•
-eq — равно•
-ne — не равно•
-gt — больше•
-lt — меньше•
-ge — больше или равно•
-le — меньше или равно
8.7 Условные конструкции: if / elif / else
Условные конструкции позволяют скрипту принимать решения и выполнять разные действия в зависимости от условий. Это фундамент любого нетривиального скрипта.
Базовый синтаксис if
#!/bin/bash
# Простое условие
if [ "$USER" = "root" ]; then
echo "Вы вошли как root"
fi
# if / else
age=20
if [ "$age" -ge 18 ]; then
echo "Вы совершеннолетний"
else
echo "Вы несовершеннолетний"
fi
# if / elif / else
score=75
if [ "$score" -ge 90 ]; then
echo "Отлично"
elif [ "$score" -ge 70 ]; then
echo "Хорошо"
elif [ "$score" -ge 50 ]; then
echo "Удовлетворительно"
else
echo "Неудовлетворительно"
fi
Скобки [ ] vs [[ ]]
В Bash существует два синтаксиса для условий. Двойные скобки [[ ]] — расширение Bash, безопаснее и мощнее.
# [ ] — POSIX-совместимый синтаксис (работает в sh)
# Обязательно ставить пробелы после [ и перед ]
if [ "$name" = "Иван" ]; then
echo "Привет, Иван!"
fi
# [[ ]] — расширение Bash (рекомендуется)
# Поддерживает && ||, регулярные выражения, глобы
if [[ "$name" == "Иван" && "$age" -gt 18 ]]; then
echo "Иван, добро пожаловать!"
fi
# Регулярные выражения (только в [[ ]])
email="user@example.com"
if [[ "$email" =~ ^[a-zA-Z0-9]+@[a-zA-Z]+\.[a-zA-Z]+$ ]]; then
echo "Email валиден"
fi
# Глобы (только в [[ ]])
file="report.pdf"
if [[ "$file" == *.pdf ]]; then
echo "Это PDF-файл"
fi
Проверка файлов
#!/bin/bash
file="/etc/passwd"
# Существует ли файл
if [ -f "$file" ]; then
echo "Файл $file существует"
fi
# Существует ли директория
if [ -d "/home/user" ]; then
echo "Директория существует"
fi
# Файл доступен для чтения / записи / выполнения
if [ -r "$file" ]; then echo "Можно читать"; fi
if [ -w "$file" ]; then echo "Можно писать"; fi
if [ -x "$file" ]; then echo "Можно выполнять"; fi
# Файл не пуст
if [ -s "$file" ]; then
echo "Файл не пуст"
fi
# Файл существует (любого типа: файл, директория, ссылка...)
if [ -e "$file" ]; then
echo "Объект существует"
fi
# Символическая ссылка
if [ -L "/usr/bin/python" ]; then
echo "Это симлинк"
fi
Проверка строк
# Строка пустая
if [ -z "$var" ]; then
echo "Переменная пустая или не задана"
fi
# Строка НЕ пустая
if [ -n "$var" ]; then
echo "Переменная содержит: $var"
fi
# Сравнение строк
if [ "$str1" = "$str2" ]; then
echo "Строки равны"
fi
if [ "$str1" != "$str2" ]; then
echo "Строки различаются"
fi
Логические операторы
# И (AND) — оба условия должны быть истинны
if [ "$age" -ge 18 ] && [ "$citizen" = "yes" ]; then
echo "Имеете право голоса"
fi
# Или (OR) — хотя бы одно условие истинно
if [ "$role" = "admin" ] || [ "$role" = "moderator" ]; then
echo "Есть права на удаление"
fi
# НЕ (NOT) — инвертирует условие
if ! [ -f "/tmp/lock" ]; then
echo "Lock-файл отсутствует, продолжаем"
fi
Однострочные условия
# Используя && и ||
[ -f "config.yml" ] && echo "Конфиг найден" || echo "Конфига нет"
# Проверка кода возврата команды
grep -q "error" logfile.txt && echo "Найдены ошибки!"
# Тест перед действием
[ -d "backup" ] || mkdir backup
• Забыть пробелы:
[$var="x"] — ОШИБКА, нужно [ "$var" = "x" ]• Забыть кавычки:
[ $var = x ] сломается если $var пустая• Использовать
== в [ ] — не POSIX, лучше =• Использовать
-eq для строк — это только для чисел
8.8 Циклы: for, while, until
Циклы позволяют выполнять одни и те же действия многократно. В bash есть три основных типа циклов.
Цикл for
#!/bin/bash
# Перебор списка значений
for fruit in яблоко груша банан; do
echo "Фрукт: $fruit"
done
# Перебор файлов
for file in *.txt; do
echo "Обрабатываю: $file"
wc -l "$file"
done
# Перебор числового диапазона
for i in {1..10}; do
echo "Итерация: $i"
done
# Диапазон с шагом
for i in {0..100..5}; do
echo "$i"
done
# C-style for (как в C/Java)
for ((i = 0; i < 10; i++)); do
echo "i = $i"
done
# Перебор аргументов скрипта
for arg in "$@"; do
echo "Аргумент: $arg"
done
# Перебор строк из команды
for user in $(cut -d: -f1 /etc/passwd); do
echo "Пользователь: $user"
done
Цикл while
#!/bin/bash
# Базовый while
counter=1
while [ "$counter" -le 5 ]; do
echo "Счётчик: $counter"
counter=$((counter + 1))
done
# Бесконечный цикл (с выходом по break)
while true; do
read -p "Введите 'quit' для выхода: " input
if [ "$input" = "quit" ]; then
break
fi
echo "Вы ввели: $input"
done
# Чтение файла построчно (правильный способ)
while IFS= read -r line; do
echo "Строка: $line"
done < "data.txt"
# Чтение вывода команды
ps aux | while read -r line; do
echo "$line"
done
# while с несколькими условиями
attempts=0
max_attempts=3
while [ "$attempts" -lt "$max_attempts" ]; do
echo "Попытка $((attempts + 1)) из $max_attempts"
attempts=$((attempts + 1))
sleep 1
done
Цикл until (пока НЕ выполнится)
#!/bin/bash
# until — выполняется пока условие ЛОЖНО
counter=1
until [ "$counter" -gt 5 ]; do
echo "Счётчик: $counter"
counter=$((counter + 1))
done
# Ожидание появления файла
until [ -f "/tmp/ready.flag" ]; do
echo "Ожидаю файл-флаг..."
sleep 2
done
echo "Файл найден! Продолжаем."
# Ожидание доступности сервиса
until ping -c 1 google.com &>/dev/null; do
echo "Нет сети, ждём..."
sleep 5
done
echo "Сеть доступна!"
break и continue
#!/bin/bash
# break — выйти из цикла
for i in {1..100}; do
if [ "$i" -eq 5 ]; then
echo "Достигнуто 5, выходим"
break
fi
echo "$i"
done
# continue — пропустить итерацию
for i in {1..10}; do
if [ $((i % 2)) -eq 0 ]; then
continue # пропускаем чётные
fi
echo "Нечётное: $i"
done
# break N — выход из вложенного цикла (N уровней)
for i in {1..3}; do
for j in {1..3}; do
if [ "$j" -eq 2 ]; then
break 2 # выйти из обоих циклов
fi
echo "$i - $j"
done
done
Практический пример: мониторинг процесса
#!/bin/bash
# Скрипт мониторинга процесса
process_name="nginx"
check_interval=10
echo "Мониторинг процесса: $process_name"
while true; do
if pgrep -x "$process_name" > /dev/null; then
echo "[$(date '+%H:%M:%S')] $process_name работает"
else
echo "[$(date '+%H:%M:%S')] $process_name НЕ НАЙДЕН! Перезапускаю..."
systemctl restart "$process_name"
fi
sleep "$check_interval"
done
8.9 Конструкции case и select
case — это мощная альтернатива цепочке if/elif для сравнения одной переменной с несколькими шаблонами. select создаёт интерактивное меню.
Конструкция case
#!/bin/bash
read -p "Введите день недели: " day
case "$day" in
понедельник|вторник|среда|четверг|пятница)
echo "Рабочий день"
;;
суббота|воскресенье)
echo "Выходной!"
;;
*)
echo "Неизвестный день: $day"
;;
esac
case с шаблонами (глобы)
#!/bin/bash
filename="$1"
case "$filename" in
*.tar.gz|*.tgz)
echo "Распаковка tar.gz..."
tar xzf "$filename"
;;
*.tar.bz2)
echo "Распаковка tar.bz2..."
tar xjf "$filename"
;;
*.zip)
echo "Распаковка zip..."
unzip "$filename"
;;
*.7z)
echo "Распаковка 7z..."
7z x "$filename"
;;
*)
echo "Неизвестный формат: $filename"
exit 1
;;
esac
case для обработки аргументов скрипта
#!/bin/bash
# Скрипт управления сервисом
case "$1" in
start)
echo "Запуск сервиса..."
;;
stop)
echo "Остановка сервиса..."
;;
restart)
echo "Перезапуск сервиса..."
;;
status)
echo "Проверка статуса..."
;;
*)
echo "Использование: $0 {start|stop|restart|status}"
exit 1
;;
esac
Конструкция select (меню)
#!/bin/bash
echo "Выберите операционную систему:"
select os in "Ubuntu" "Fedora" "Arch" "Выход"; do
case "$os" in
Ubuntu)
echo "Менеджер пакетов: apt"
;;
Fedora)
echo "Менеджер пакетов: dnf"
;;
Arch)
echo "Менеджер пакетов: pacman"
;;
Выход)
echo "До свидания!"
break
;;
*)
echo "Неверный выбор, попробуйте снова"
;;
esac
done
8.10 Функции
Функции позволяют группировать команды, давать им имя и переиспользовать. Это делает скрипты читаемыми и поддерживаемыми.
Объявление и вызов
#!/bin/bash
# Способ 1: с ключевым словом function
function greet() {
echo "Привет, $1!"
}
# Способ 2: без function (POSIX-совместимый)
say_bye() {
echo "Пока, $1!"
}
# Вызов функций
greet "Иван" # Привет, Иван!
say_bye "Мария" # Пока, Мария!
Аргументы функций
#!/bin/bash
create_user() {
local username="$1" # local — переменная видна только внутри функции
local fullname="$2"
local role="${3:-user}" # значение по умолчанию
echo "Создаю пользователя:"
echo " Логин: $username"
echo " Имя: $fullname"
echo " Роль: $role"
echo " Всего аргументов: $#"
}
create_user "ivanov" "Иван Иванов" "admin"
create_user "petrov" "Пётр Петров" # роль = "user" (по умолчанию)
Возвращаемые значения
#!/bin/bash
# Способ 1: return (код возврата 0-255)
is_root() {
if [ "$(id -u)" -eq 0 ]; then
return 0 # успех (true)
else
return 1 # неудача (false)
fi
}
if is_root; then
echo "Вы root"
else
echo "Вы НЕ root"
fi
# Способ 2: echo (для возврата строк/чисел)
get_disk_usage() {
local usage
usage=$(df -h / | awk 'NR==2 {print $5}')
echo "$usage"
}
disk=$(get_disk_usage)
echo "Использование диска: $disk"
# Способ 3: глобальная переменная (менее предпочтительно)
calculate() {
RESULT=$(( $1 + $2 ))
}
calculate 10 20
echo "Результат: $RESULT"
Область видимости: local
#!/bin/bash
name="Глобальная"
test_scope() {
local name="Локальная"
echo "Внутри функции: $name" # Локальная
}
test_scope
echo "Снаружи функции: $name" # Глобальная
# ВАЖНО: без local переменная будет глобальной!
bad_function() {
result="изменено" # это ГЛОБАЛЬНАЯ переменная
}
result="оригинал"
bad_function
echo "$result" # "изменено" — переменная была затёрта!
Практический пример: библиотека функций
#!/bin/bash
# Файл: lib.sh — библиотека полезных функций
log_info() {
echo "[INFO $(date '+%Y-%m-%d %H:%M:%S')] $*"
}
log_error() {
echo "[ERROR $(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
}
file_exists() {
[ -f "$1" ]
}
require_root() {
if [ "$(id -u)" -ne 0 ]; then
log_error "Этот скрипт требует прав root"
exit 1
fi
}
# В другом скрипте подключаем библиотеку:
# source lib.sh
# log_info "Скрипт запущен"
# require_root
• Всегда используйте
local для переменных внутри функций• Давайте функциям говорящие имена:
validate_email, а не check• Документируйте функции комментариями
• Выносите повторяющийся код в функции
8.11 Массивы
Bash поддерживает индексированные и ассоциативные массивы. Они позволяют хранить коллекции значений в одной переменной.
Индексированные массивы
#!/bin/bash
# Создание массива
fruits=("яблоко" "груша" "банан" "апельсин")
# Доступ к элементам (индексы с 0)
echo "${fruits[0]}" # яблоко
echo "${fruits[2]}" # банан
# Все элементы
echo "${fruits[@]}" # яблоко груша банан апельсин
# Количество элементов
echo "${#fruits[@]}" # 4
# Длина конкретного элемента
echo "${#fruits[0]}" # 6 (длина слова "яблоко")
# Добавить элемент
fruits+=("манго")
# Удалить элемент
unset 'fruits[1]' # удаляет "груша"
# Срез массива (элементы 1-2)
echo "${fruits[@]:1:2}"
Перебор массива
#!/bin/bash
servers=("web-01" "web-02" "db-01" "cache-01")
# Перебор значений
for server in "${servers[@]}"; do
echo "Проверяю сервер: $server"
ping -c 1 "$server" &>/dev/null && echo " OK" || echo " НЕДОСТУПЕН"
done
# Перебор с индексами
for i in "${!servers[@]}"; do
echo "Сервер #$i: ${servers[$i]}"
done
Ассоциативные массивы (Bash 4+)
#!/bin/bash
# Объявление (обязательно declare -A)
declare -A config
config[host]="localhost"
config[port]="8080"
config[db]="myapp"
config[debug]="true"
# Доступ
echo "Хост: ${config[host]}"
echo "Порт: ${config[port]}"
# Все ключи
echo "Ключи: ${!config[@]}"
# Все значения
echo "Значения: ${config[@]}"
# Перебор
for key in "${!config[@]}"; do
echo "$key = ${config[$key]}"
done
# Проверка существования ключа
if [[ -v config[debug] ]]; then
echo "Ключ debug существует"
fi
Практический пример: парсинг CSV
#!/bin/bash
# Чтение CSV-файла в массивы
declare -a names
declare -a ages
i=0
while IFS=',' read -r name age; do
names[$i]="$name"
ages[$i]="$age"
((i++))
done < "users.csv"
echo "Загружено ${#names[@]} записей"
for i in "${!names[@]}"; do
echo "${names[$i]} — ${ages[$i]} лет"
done
8.12 Работа со строками
Bash предоставляет множество встроенных операций для манипуляции строками без вызова внешних утилит.
Подстроки и замены
#!/bin/bash
str="Hello World Bash Scripting"
# Длина строки
echo "${#str}" # 25
# Подстрока (с позиции 6, 5 символов)
echo "${str:6:5}" # World
# С конца
echo "${str: -9}" # Scripting
# Удаление с начала (кратчайшее совпадение)
file="archive.tar.gz"
echo "${file#*.}" # tar.gz
# Удаление с начала (длиннейшее совпадение)
echo "${file##*.}" # gz
# Удаление с конца (кратчайшее)
echo "${file%.*}" # archive.tar
# Удаление с конца (длиннейшее)
echo "${file%%.*}" # archive
# Замена первого вхождения
echo "${str/World/Мир}" # Hello Мир Bash Scripting
# Замена всех вхождений
text="a-b-c-d"
echo "${text//-/_}" # a_b_c_d
# Приведение к верхнему/нижнему регистру (Bash 4+)
name="hello world"
echo "${name^^}" # HELLO WORLD
echo "${name^}" # Hello world (только первая буква)
upper="HELLO"
echo "${upper,,}" # hello
echo "${upper,}" # hELLO
Значения по умолчанию
# Если переменная не задана — использовать значение по умолчанию
echo "${NAME:-Гость}" # "Гость" если NAME пустая
# Если не задана — присвоить и использовать
echo "${NAME:=Гость}" # присваивает NAME="Гость"
# Если не задана — вывести ошибку и завершить
echo "${NAME:?Переменная NAME обязательна}"
# Если задана — использовать альтернативное значение
echo "${NAME:+Пользователь задан}"
8.13 Обработка ошибок: set, trap, коды возврата
Надёжный скрипт должен корректно обрабатывать ошибки. Bash предоставляет для этого несколько механизмов.
set — строгий режим
#!/bin/bash
set -euo pipefail
# set -e : завершить скрипт при первой ошибке (ненулевой exit code)
# set -u : считать неинициализированные переменные ошибкой
# set -o pipefail : ошибка в любой части пайпа = ошибка всего пайпа
# Рекомендуется ставить в начале КАЖДОГО скрипта!
# Пример: без set -e скрипт продолжит выполнение после ошибки
cd /nonexistent/path # ← скрипт ОСТАНОВИТСЯ здесь
echo "Эта строка не выполнится"
trap — обработка сигналов
#!/bin/bash
set -euo pipefail
# Создаём временный файл
tmpfile=$(mktemp)
echo "Временный файл: $tmpfile"
# trap — выполнить команду при выходе (даже при ошибке)
trap 'rm -f "$tmpfile"; echo "Очистка завершена"' EXIT
# trap для конкретного сигнала
trap 'echo "Получен SIGINT (Ctrl+C), завершаюсь..."; exit 1' INT
trap 'echo "Получен SIGTERM"; exit 1' TERM
# Основная логика скрипта
echo "Работаю..."
echo "Данные" > "$tmpfile"
sleep 10 # Попробуйте нажать Ctrl+C
# При выходе (нормальном или аварийном) tmpfile будет удалён
Коды возврата и обработка ошибок
#!/bin/bash
# $? — код возврата последней команды (0 = успех)
ls /etc/passwd
echo "Код возврата: $?" # 0
ls /nonexistent 2>/dev/null
echo "Код возврата: $?" # 2 (не найдено)
# Обработка ошибок с ||
cp important.txt backup/ || {
echo "Ошибка копирования!"
exit 1
}
# Функция для обработки ошибок
die() {
echo "ОШИБКА: $*" >&2
exit 1
}
[ -f "config.yml" ] || die "Файл config.yml не найден"
[ -d "output" ] || die "Директория output не существует"
Перенаправление ошибок
# stderr (поток 2) в файл
command 2> errors.log
# stderr и stdout в один файл
command &> output.log
# или
command > output.log 2>&1
# Подавить все ошибки
command 2>/dev/null
# Логирование с разделением потоков
./script.sh > stdout.log 2> stderr.log
set -euo pipefail! Это предотвращает большинство скрытых ошибок. Без этого скрипт молча продолжит выполнение после ошибки, что может привести к потере данных.
8.14 Практика: реальные скрипты
Закрепим все изученные концепции на примерах реальных скриптов, которые полезны в повседневной работе.
Скрипт 1: Автоматический бэкап
#!/bin/bash
set -euo pipefail
# Конфигурация
SOURCE_DIR="/home/user/documents"
BACKUP_DIR="/backup"
MAX_BACKUPS=7
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="backup_${DATE}.tar.gz"
log() { echo "[$(date '+%H:%M:%S')] $*"; }
log "Начинаю резервное копирование..."
# Проверки
[ -d "$SOURCE_DIR" ] || { log "ОШИБКА: $SOURCE_DIR не найден"; exit 1; }
mkdir -p "$BACKUP_DIR"
# Создание архива
tar czf "${BACKUP_DIR}/${BACKUP_NAME}" -C "$(dirname "$SOURCE_DIR")" "$(basename "$SOURCE_DIR")"
log "Архив создан: $BACKUP_NAME ($(du -h "${BACKUP_DIR}/${BACKUP_NAME}" | cut -f1))"
# Ротация: удаляем старые бэкапы
backup_count=$(ls -1 "${BACKUP_DIR}"/backup_*.tar.gz 2>/dev/null | wc -l)
if [ "$backup_count" -gt "$MAX_BACKUPS" ]; then
to_delete=$((backup_count - MAX_BACKUPS))
ls -1t "${BACKUP_DIR}"/backup_*.tar.gz | tail -n "$to_delete" | xargs rm -f
log "Удалено $to_delete старых бэкапов"
fi
log "Готово! Всего бэкапов: $(ls -1 "${BACKUP_DIR}"/backup_*.tar.gz | wc -l)"
Скрипт 2: Проверка здоровья серверов
#!/bin/bash
set -euo pipefail
servers=("web-01.example.com" "web-02.example.com" "db-01.example.com")
declare -A results
check_server() {
local server="$1"
if ping -c 1 -W 2 "$server" &>/dev/null; then
echo "UP"
else
echo "DOWN"
fi
}
echo "=== Проверка серверов: $(date) ==="
echo
for server in "${servers[@]}"; do
status=$(check_server "$server")
results["$server"]="$status"
if [ "$status" = "UP" ]; then
echo " [OK] $server"
else
echo " [FAIL] $server"
fi
done
# Подсчёт
up=0; down=0
for status in "${results[@]}"; do
[ "$status" = "UP" ] && ((up++)) || ((down++))
done
echo
echo "Итого: $up работают, $down недоступны из ${#servers[@]}"
Скрипт 3: Парсинг аргументов командной строки
#!/bin/bash
set -euo pipefail
# Значения по умолчанию
VERBOSE=false
OUTPUT_DIR="./output"
INPUT_FILE=""
usage() {
cat <<EOF
Использование: $(basename "$0") [ОПЦИИ] ФАЙЛ
Опции:
-o, --output DIR Выходная директория (по умолчанию: ./output)
-v, --verbose Подробный вывод
-h, --help Показать справку
Пример:
$(basename "$0") -v -o /tmp/results data.csv
EOF
exit 0
}
# Парсинг аргументов
while [[ $# -gt 0 ]]; do
case "$1" in
-o|--output)
OUTPUT_DIR="$2"
shift 2
;;
-v|--verbose)
VERBOSE=true
shift
;;
-h|--help)
usage
;;
-*)
echo "Неизвестная опция: $1" >&2
usage
;;
*)
INPUT_FILE="$1"
shift
;;
esac
done
# Проверки
[ -n "$INPUT_FILE" ] || { echo "Ошибка: не указан входной файл" >&2; usage; }
[ -f "$INPUT_FILE" ] || { echo "Ошибка: файл $INPUT_FILE не найден" >&2; exit 1; }
# Основная логика
$VERBOSE && echo "Входной файл: $INPUT_FILE"
$VERBOSE && echo "Выходная директория: $OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR"
echo "Обработка $INPUT_FILE..."
1. Напишите скрипт, который принимает директорию и выводит топ-10 файлов по размеру
2. Создайте скрипт мониторинга использования диска с уведомлением при > 80%
3. Напишите скрипт, который переименовывает все .JPG файлы в .jpg (нижний регистр)
4. Создайте скрипт автоматической ротации логов с архивированием старых