$ 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 мин·98
Пишем корректные 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 – для начинающих!

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

Перейти к курсу
Solidity DLC - поймай, если сможешь

Solidity DLC - поймай, если сможешь

Аудит смарт-контрактов на реальных взломах. The DAO, Parity, Wormhole, Ronin, Euler и ещё 7 разборов: что пошло не так, сколько потеряли и что бы спасло. Атаки и защиты на Foundry, план входа в профессию аудитора.

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

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

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

Перейти к курсу
Solidity pro - всё или ничего

Solidity pro - всё или ничего

Как пишут контракты, которые работают в основной сети. Наследование и OpenZeppelin, паттерны продакшена, оптимизация газа, ABI, delegatecall и подписи. 7 модулей, 33 урока, а в финале собственное dApp: мини-DAO с токеном, голосованием и казной.

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

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

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

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

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

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

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

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

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

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

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

21 сентября 2026 г.
1960
Как начать с HTML и сделать первую страницу: пошагово и без сложных слов
html

Как начать с HTML и сделать первую страницу: пошагово и без сложных слов

Разберём, что нужно новичку, чтобы за 1–2 вечера собрать свою первую веб-страницу: структура HTML, базовые теги, ссылки и картинки, плюс простой чек-лист, как понять, что страница работает правильно.

2 ноября 2026 г.
20
Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей
cargo

Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей

Разберём, как организовать Rust workspace, управлять optional features, фиксировать версии библиотек и уменьшать “дрейф” зависимостей при командной разработке и CI.

18 сентября 2026 г.
1960
Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах
такое

Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах

Разберём, из чего состоит веб-страница: что делает HTML (смысл и структура) и что делает CSS (оформление). В конце соберёте мини-страницу и поймёте, что нужно учить в каком порядке.

30 октября 2026 г.
130

Комментарии

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

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

Содержание

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