$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогПишем корректные bash-скрипты: обработка ошибок, строгие режимы и безопасные переменные

Пишем корректные bash-скрипты: обработка ошибок, строгие режимы и безопасные переменные

$ sudo teach IT
·24 июля 2026 г.·10 мин·72
Пишем корректные bash-скрипты: обработка ошибок, строгие режимы и безопасные переменные

Покажем best practices: set -euo pipefail, ловля ошибок, корректное quoting и проверка входных параметров. Разберём мини-проект с устойчивым поведением.

Содержание
Почему bash-скрипты “ломаются” и что именно нужно исправитьСтрогие режимы bash: set -euo pipefail и их реальное значениеset -e: “останавливаться на ошибках”, но не всегда так, как ожидаютset -u: запрет на неинициализированные переменныеset -o pipefail: в конвейерах важна первая ошибкаПолитика ошибок: единая функция, trap и контролируемый выходБазовый шаблон: обработчик выхода + очисткаБезопасные переменные и правильные кавычки: где чаще всего ломается реальная практикаПринцип: всегда цитируйте переменные, которые могут содержать пробелы-- после опций: защита от “похожих на флаги” значенийОпасные расширения: globbing и word splittingВалидация входных параметров: строгие контракты вместо “надеемся, что всё хорошо”Пример: простой валидатор аргументовМини-проект: устойчивый скрипт обработки логов (с тестируемым поведением)Требования к поведениюРеализацияЧто здесь “сделано правильно” (и почему)Типичные ошибки, которые ломают строгие скрипты (и как их избегать)Ошибка 1: думать, что set -e “всегда” ловит любые проблемы

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

Хорошая новость: устойчивое поведение bash-скриптов достигается набором практик, которые давно считаются best practices в экосистеме Linux. В этой статье разберём их системно: строгие режимы (set -euo pipefail), предсказуемая обработка ошибок, правильные quoting, безопасная работа с переменными, валидация аргументов и построение мини-проекта “по-взрослому”.


Почему bash-скрипты “ломаются” и что именно нужно исправить

Типовые источники проблем в bash:

  1. Отсутствие остановки на ошибках

    • Команда завершилась ненулевым кодом, но скрипт продолжил работу.
    • В конвейерах (cmd1 | cmd2) ошибка cmd1 может остаться незамеченной, если cmd2 успешно завершается.
  2. Неинициализированные переменные

    • Переменная не задана — bash подставляет пустую строку или интерпретирует выражения иначе, чем ожидается.
    • Например, rm -rf "$path" с пустым $path превращается в опасную операцию.
  3. Неправильный quoting

    • Пути и строки с пробелами/табами/символами интерпретируются как несколько аргументов.
    • В худшем случае — подмена данных при подстановках и расширениях.
  4. Отсутствие проверок входных параметров

    • Скрипт ожидает аргументы определённого формата, но не валидирует их.
    • Ошибка обнаруживается поздно, в середине выполнения, когда уже есть частичные изменения.
  5. Сложные сценарии и отсутствие логирования

    • Ошибка произошла внутри функции/конвейера, но понять контекст сложно.
    • Без корректных хендлеров (trap) тяжело обеспечить очистку временных файлов и корректный выход.

Решение — дисциплина: строгие режимы, явная политика обработки ошибок, централизованный вывод диагностик и предсказуемая работа с данными.


Строгие режимы bash: set -euo pipefail и их реальное значение

Начнём с основы, которую часто копируют из статей. Но важно понимать, что именно она делает.

set -e: “останавливаться на ошибках”, но не всегда так, как ожидают

set -e заставляет bash завершаться при неуспешном выполнении простых команд. Однако есть нюансы: в некоторых конструкциях bash намеренно не включает “авто-выход”, чтобы не ломать логику условий. Например, в if cmd; then ... fi ошибка cmd не приводит к немедленному завершению — это ожидаемо, потому что ошибка используется как условие.

Практический вывод: set -e полезен как “предохранитель”, но не заменяет явную проверку там, где вы хотите особую семантику.

set -u: запрет на неинициализированные переменные

set -u (или nounset) делает попытку использовать несуществующую переменную ошибкой. Это значительно снижает количество “молчаливых” багов, когда забыли задать аргумент или переменную.

Например, без -u выражение вроде:

  • echo "$version" → напечатает пусто, и скрипт может пойти дальше.
  • rm -rf "$dir" при пустом $dir — потенциально катастрофично.

С -u вы получите быстрый фейл и сможете отлавливать ошибку ближе к источнику.

set -o pipefail: в конвейерах важна первая ошибка

По умолчанию bash возвращает код последней команды в конвейере. При cmd1 | cmd2 вы можете получить успешный код даже если cmd1 упал.

set -o pipefail изменяет поведение: конвейер возвращает ненулевой код, если любая команда в конвейере завершилась с ошибкой (точнее — возвращается код последней неуспешной команды).

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


Политика ошибок: единая функция, trap и контролируемый выход

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

Базовый шаблон: обработчик выхода + очистка

В bash удобно использовать trap на EXIT и/или ERR.

  • trap ... EXIT — сработает при любом завершении скрипта.
  • trap ... ERR — срабатывает на ошибке команд при включённом set -e (с нюансами), позволяя зафиксировать диагностику.

Рассмотрим рабочий каркас:

code
#!/usr/bin/env bash
set -euo pipefail

# Локализация временных артефактов
tmpdir=""
cleanup() {
  # Безопасная очистка только если директория реально создана
  if [[ -n "${tmpdir}" && -d "${tmpdir}" ]]; then
    rm -rf -- "${tmpdir}"
  fi
}

on_err() {
  local exit_code=$?
  local line_no=${BASH_LINENO[0]:-unknown}
  local cmd=${BASH_COMMAND:-unknown}

  printf 'ERROR: command failed (exit=%s) at line %s: %s\n' \
    "${exit_code}" "${line_no}" "${cmd}" >&2
}

trap cleanup EXIT
trap on_err ERR

Что здесь важно:

  • tmpdir объявлен и инициализирован, что предотвращает ошибки из-за set -u.
  • В cleanup проверяем наличие и тип директории.
  • Диагностика печатает код возврата и строку, что помогает быстро локализовать проблему.
  • printf вместо echo (у echo есть исторические отличия в поведении, зависящие от реализации).

Безопасные переменные и правильные кавычки: где чаще всего ломается реальная практика

Принцип: всегда цитируйте переменные, которые могут содержать пробелы

Если переменная — это аргумент команды, почти всегда нужно писать:

  • "$var" вместо $var
  • "${arr[@]}" вместо ${arr[@]}

Пример типичной ошибки:

code
path=/tmp/my dir
rm -rf $path   # ❌ разделит на два аргумента: /tmp/my и dir

Правильно:

code
rm -rf -- "$path"  # ✅ один аргумент, и -- защищает от строк вида -rf

-- после опций: защита от “похожих на флаги” значений

Если пользователь передаст путь вида -rf, команда может воспринять его как опцию. Чтобы избежать этого, большинство утилит принимают -- как конец опций:

code
cp -- "$src" "$dst"
rm -rf -- "$path"
mkdir -p -- "$dir"

Это особенно важно в “утилитарных” скриптах, которые могут быть использованы в разных контекстах.

Опасные расширения: globbing и word splitting

Два механизма, которые часто приводят к неожиданностям:

  • Word splitting: строка разбивается по пробельным символам, если она не заключена в кавычки.
  • Globbing: шаблоны *, ?, [] расширяются по файловой системе.

Кавычки отключают оба эффекта для содержимого переменной.


Валидация входных параметров: строгие контракты вместо “надеемся, что всё хорошо”

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

  1. Явно задокументировать назначение параметров.
  2. Проверить количество аргументов.
  3. Проверить тип/формат (например, “директория существует”, “число в диапазоне”, “файл читаем”).
  4. Отдельно учесть крайние случаи: пустые строки, слишком длинные значения, неожиданные символы.

Пример: простой валидатор аргументов

Предположим, скрипт должен принимать:

  • --input путь к файлу
  • --output путь к выходному файлу
  • --pattern шаблон поиска (regex)
  • --min-bytes минимальный размер (число)

Каркас разбора аргументов можно сделать через getopts или вручную. Для гибкости часто делают вручную, но с аккуратной проверкой.


Мини-проект: устойчивый скрипт обработки логов (с тестируемым поведением)

Соберём практический пример. Задача:

  • Считать входной лог-файл.
  • Отфильтровать строки по регулярному выражению.
  • Записать результат в выходной файл.
  • Параметр --min-bytes задаёт минимальный размер входа, чтобы скрипт не обрабатывал “пустой” лог.
  • Скрипт должен вести себя предсказуемо при ошибках: корректный код выхода, понятная диагностика, очистка временных файлов.

Требования к поведению

  1. Если входной файл не существует или недоступен — скрипт завершится с ошибкой до начала работы.
  2. Если --min-bytes не число — ошибка.
  3. Если regex не валиден для grep -E — ошибка должна быть диагностирована.
  4. Временные файлы должны удаляться при любой ошибке.
  5. Пустые результаты фильтрации — допустимы (например, если нет совпадений), но это должно быть явно отражено в сообщении.

Реализация

code
#!/usr/bin/env bash
set -euo pipefail

tmpdir=""

cleanup() {
  if [[ -n "${tmpdir}" && -d "${tmpdir}" ]]; then
    rm -rf -- "${tmpdir}"
  fi
}

on_err() {
  local exit_code=$?
  local line_no=${BASH_LINENO[0]:-unknown}
  local cmd=${BASH_COMMAND:-unknown}

  printf 'ERROR: exit=%s at line=%s: %s\n' \
    "${exit_code}" "${line_no}" "${cmd}" >&2
}

trap cleanup EXIT
trap on_err ERR

usage() {
  cat >&2 <<'EOF'
Usage:
  logfilter.sh --input <file> --output <file> --pattern <regex> --min-bytes <N>

Notes:
  - pattern is used with grep -E
  - script writes matches to output
EOF
}

# По умолчанию не задаём значения, чтобы set -u не скрывал ошибки
input=""
output=""
pattern=""
min_bytes=""

# Простейший парсер аргументов
while [[ $# -gt 0 ]]; do
  case "$1" in
    --input)
      input="${2:-}"; shift 2 ;;
    --output)
      output="${2:-}"; shift 2 ;;
    --pattern)
      pattern="${2:-}"; shift 2 ;;
    --min-bytes)
      min_bytes="${2:-}"; shift 2 ;;
    -h|--help)
      usage; exit 0 ;;
    *)
      printf 'Unknown argument: %s\n' "$1" >&2
      usage
      exit 2 ;;
  esac
done

# Проверка обязательных параметров
if [[ -z "${input}" || -z "${output}" || -z "${pattern}" || -z "${min_bytes}" ]]; then
  printf 'Missing required arguments.\n' >&2
  usage
  exit 2
fi

# Валидация входного файла
if [[ ! -f "${input}" ]]; then
  printf 'Input file does not exist: %s\n' "${input}" >&2
  exit 2
fi
if [[ ! -r "${input}" ]]; then
  printf 'Input file is not readable: %s\n' "${input}" >&2
  exit 2
fi

# Проверка min_bytes: только целое число >= 0
if [[ "${min_bytes}" =~ ^[0-9]+$ ]]; then
  : # ок
else
  printf '--min-bytes must be a non-negative integer, got: %s\n' "${min_bytes}" >&2
  exit 2
fi

# Проверка размера файла
# GNU stat: %s — размер в байтах
# (Если вы работаете на macOS, потребуется другая команда; тут намеренно упрощаем)
file_size="$(stat -c '%s' -- "${input}")"
if (( file_size < min_bytes )); then
  printf 'Input is too small: %s bytes < %s bytes\n' "${file_size}" "${min_bytes}" >&2
  exit 1
fi

# Создаём tempdir
tmpdir="$(mktemp -d)"

# Здесь можно предварительно проверить корректность regex:
# grep -E проверяет regex и возвращает код.
# Но grep возвращает 1 и при "нет совпадений", поэтому нам нужен тест на синтаксическую ошибку.
# Простой способ: прогнать regex на пустой ввод и смотреть код — синтаксическая ошибка даст код 2.
# Однако поведение grep зависит от версии; ниже — практичный вариант:
if ! printf '' | grep -E -- "${pattern}" >/dev/null; then
  # Код 1 = нет совпадений, код 2 = ошибка regex (обычно)
  # На разных grep точные коды могут отличаться, поэтому мы проверяем текстом:
  # Если regex синтаксически неверен, grep обычно пишет сообщение в stderr.
  # Поэтому делаем ещё один проход с перехватом.
  if err_msg="$( (printf '' | grep -E -- "${pattern}") 2>&1 >/dev/null; echo $? )" ; then
    : # не используем; оставляем ниже другой вариант
  fi
fi

# Надёжнее: попытаться сделать фильтрацию и доверять stderr grep.
# Если regex синтаксически неверен — grep вернёт ошибку.
out_tmp="${tmpdir}/out.txt"

# grep: -E расширенные регулярные выражения
# -n не включаем, потому что формат должен остаться исходным
# Важно: set -e + отсутствие совпадений
# grep при отсутствии совпадений возвращает 1 -> set -e немедленно завершит скрипт.
# Поэтому нужно обработать это отдельно.
set +e
grep -E -- "${pattern}" -- "${input}" > "${out_tmp}"
grep_status=$?
set -e

if (( grep_status == 0 )); then
  : # совпадения есть
elif (( grep_status == 1 )); then
  # нет совпадений — считаем это допустимым исходом
  printf 'No matches found for pattern in %s. Writing empty output.\n' "${input}" >&2
else
  # код 2 и выше — ошибка
  printf 'grep failed with status %s. Check regex/pattern and input.\n' "${grep_status}" >&2
  exit "${grep_status}"
fi

# Атомарная запись (сначала во временный, потом переместить)
# Используем mv -f, чтобы избежать проблем с частичной записью в output.
mv -f -- "${out_tmp}" "${output}"

printf 'Done. Output written to %s\n' "${output}" >&2

Что здесь “сделано правильно” (и почему)

1) Строгие режимы включены с самого начала

set -euo pipefail предотвращает большинство типовых тихих ошибок. Но из-за set -e есть важный нюанс с grep: отсутствие совпадений возвращает код 1 и может прервать скрипт. Мы это обрабатываем аккуратно через временное отключение set -e на участке:

  • set +e
  • выполнение grep
  • анализ grep_status
  • возвращение set -e

Это не “обход строгих режимов”, а точечная настройка семантики: “код 1 для grep означает допустимый кейс”.

2) Все переменные цитируются

"${input}", "${pattern}", "${out_tmp}" — скрипт корректно обрабатывает пробелы и спецсимволы в путях/паттернах (насколько это допускает grep и shell).

3) Проверяется формат --min-bytes

Без этого легко получить неожиданное сравнение в арифметике. С regex ^[0-9]+$ мы гарантируем, что (( file_size < min_bytes )) не превратится в “сравнение с мусором”.

4) Временные файлы очищаются гарантированно

trap cleanup EXIT удаляет tempdir даже при раннем падении на ошибках. Это особенно важно, если скрипт часто запускается в CI или как часть пайплайна.


Типичные ошибки, которые ломают строгие скрипты (и как их избегать)

Ошибка 1: думать, что set -e “всегда” ловит любые проблемы

set -e имеет нюансы внутри условных конструкций, циклов и списков команд. Поэтому для “критичных” операций полезно добавлять явные проверки. Пример:

  • если нужна конкретная семантика (“код 1 означает пустой результат”), то надо обрабатывать код.
  • если нужна “жёсткая” семантика, то можно позволить set -e

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Курс по теме

Изучаем bash

Разработка скриптов на bash

Бесплатно
Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

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

Перейти к курсу
Приложения для iPhone и Apple Watch на SwiftUI

Приложения для iPhone и Apple Watch на SwiftUI

Разработка приложений для iPhone и Apple Watch на SwiftUI: навигация, SwiftData, виджеты, часы, выпуск. Нужен Mac с Xcode 27, сами устройства не нужны.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Приложения для macOS на SwiftUI

Приложения для macOS на SwiftUI

Разработка приложений для Mac на SwiftUI: окна и меню, Liquid Glass, SwiftData, сеть, выпуск. Нужен Mac с macOS 27 и Xcode 27.

Перейти к курсу

Другие статьи

Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта
пишем

Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта

Построим удобный интерфейс командной утилиты: структура подкоманд, чтение конфигов, валидация входных параметров и единая логика ошибок с понятными exit code. Сфокусируемся на UX для тех, кто запускает CLI в CI и в скриптах.

15 сентября 2026 г.
1010
Generics в TypeScript: пишем универсальный код без потери типобезопасности
generics

Generics в TypeScript: пишем универсальный код без потери типобезопасности

Разбираем обобщённые типы на практике — от простых функций до сложных ограничений с extends и keyof. После статьи вы перестанете избегать дженериков и начнёте использовать их осознанно.

18 июля 2026 г.
700
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1421
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

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

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1070
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

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

2 октября 2026 г.
620

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Почему bash-скрипты “ломаются” и что именно нужно исправитьСтрогие режимы bash: set -euo pipefail и их реальное значениеset -e: “останавливаться на ошибках”, но не всегда так, как ожидаютset -u: запрет на неинициализированные переменныеset -o pipefail: в конвейерах важна первая ошибкаПолитика ошибок: единая функция, trap и контролируемый выходБазовый шаблон: обработчик выхода + очисткаБезопасные переменные и правильные кавычки: где чаще всего ломается реальная практикаПринцип: всегда цитируйте переменные, которые могут содержать пробелы-- после опций: защита от “похожих на флаги” значенийОпасные расширения: globbing и word splittingВалидация входных параметров: строгие контракты вместо “надеемся, что всё хорошо”Пример: простой валидатор аргументовМини-проект: устойчивый скрипт обработки логов (с тестируемым поведением)Требования к поведениюРеализацияЧто здесь “сделано правильно” (и почему)Типичные ошибки, которые ломают строгие скрипты (и как их избегать)Ошибка 1: думать, что set -e “всегда” ловит любые проблемы