Linux

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
Важно: В bash переменные не имеют типов. Все значения — строки. Для математических операций используются специальные конструкции.

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. Создайте скрипт автоматической ротации логов с архивированием старых

Настройки

Цветовая схема

Тема