惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

U
Unit 42
博客园 - Franky
T
Tailwind CSS Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
月光博客
月光博客
人人都是产品经理
人人都是产品经理
雷峰网
雷峰网
Hugging Face - Blog
Hugging Face - Blog
有赞技术团队
有赞技术团队
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
阮一峰的网络日志
阮一峰的网络日志
C
Check Point Blog
爱范儿
爱范儿
T
The Blog of Author Tim Ferriss
aimingoo的专栏
aimingoo的专栏
Stack Overflow Blog
Stack Overflow Blog
博客园 - 聂微东
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
L
LangChain Blog
云风的 BLOG
云风的 BLOG
MyScale Blog
MyScale Blog
Microsoft Security Blog
Microsoft Security Blog
The Cloudflare Blog
博客园 - 三生石上(FineUI控件)

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Пять проблем Bash, которые ломают скрипты в самый неудачн...
artem · 2026-06-02 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение5 мин

Охват и читатели3.9K

Туториал

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

Pipe в while‑цикл теряет переменные

Классический скрипт‑обходчик файлов:

#!/bin/bash
counter=0
find /var/log -name '*.log' | while read -r f; do
    counter=$((counter + 1))
done
echo "Found $counter files"

Что ожидается: counter содержит число найденных файлов. Что выводится: Found 0 files.

Причина — pipe | в Bash порождает подоболочку (subshell) для правой части. Цикл while крутится в этой подоболочке, и counter в ней увеличивается, но как только цикл закончился, подоболочка умирает, а изменения в переменной с ней. Родительская оболочка видит counter=0.

Лечится через process substitution, который позволяет обойтись без pipe:

#!/bin/bash
counter=0
while read -r f; do
    counter=$((counter + 1))
done < <(find /var/log -name '*.log')
echo "Found $counter files"

Конструкция < <(...) подаёт вывод команды на stdin цикла, но сам цикл выполняется в основной оболочке, и все изменения переменных сохраняются. Альтернатива — shopt -s lastpipe в Bash 4.2 и выше, который заставляет последнюю команду pipeline выполниться в основной оболочке, но эта опция влияет на весь скрипт, и применять её имеет смысл с пониманием побочных эффектов.

set ‑e не срабатывает там, где вы рассчитываете

set -e (он же errexit) выглядит как «упасть на любой ошибке». На практике у него много исключений.

set -e не падает, если команда стоит в условии if, while, until, либо слева от && или ||, либо запущена в pipeline без set -o pipefail, либо является аргументом !.

Пример, который выглядит безопасно:

#!/bin/bash
set -e

risky_command || handle_error
do_other_work

Если risky_command упадёт, выполнится handle_error, и set -e НЕ остановит скрипт. Дальше пойдёт do_other_work, как будто всё хорошо, а это часто не то, что хотел автор.

Использовать set -e имеет смысл вместе с set -o pipefail, помнить список исключений и для критичных мест ставить явные проверки if ! cmd; then echo "..." >&2; exit 1; fi. Полагаться на set -e как на полноценную обработку ошибок — неверная стратегия.

Полезная пара для верхней части любого нетривиального скрипта:

set -euo pipefail
IFS=$'\n\t'

Что добавляет: set -u — падать при использовании необъявленной переменной. pipefail заставляет pipeline возвращать первую ненулевую ошибку, а не код последней команды. IFS=$'\n\t' убирает пробел из разделителей, что снимает целый класс багов с word splitting.

((x++)) валит скрипт с set ‑e

В арифметическом расширении есть тонкая засада: ((expr)) возвращает ненулевой код, если результат выражения — ноль, и это сделано намеренно, чтобы ((x)) можно было использовать как тест на ноль.

Но это значит, что ((counter++)) при counter=0 вернёт код 1 и под set -e уронит скрипт:

#!/bin/bash
set -e
counter=0
((counter++))   # скрипт падает здесь, потому что counter был 0
echo "Survived"

Survived в выводе вы не увидите, потому что counter++ означает «вернуть текущее значение (0), потом увеличить», текущее значение 0 становится результатом выражения, код возврата 1, и set -e срабатывает.

Лечится использованием ((counter+=1)) (результат всегда ненулевой) или присваиванием через $((...)):

((counter+=1))
# или
counter=$((counter + 1))
# или, если очень хочется ++, с явным подавлением:
((counter++)) || true

Третий вариант — самый адекватный: вы явно говорите, что знаете про этот return code и сознательно его игнорируете.

Trap не наследуется в subshell

Trap, установленный в основной оболочке, не виден в подоболочках:

#!/bin/bash
cleanup() { rm -f /tmp/work.lock; }
trap cleanup EXIT

(
    # subshell
    do_something_that_might_fail
)

Если do_something_that_might_fail упадёт внутри подоболочки, cleanup не вызовется по выходе из подоболочки — он вызовется только по выходу из родительской оболочки. Это, как правило, не то, что хочет автор. Аналогично работает с bash -c '...' и ssh user@host 'script' — это новые процессы, родительские trap'ы туда не приходят.

Trap внутри подоболочки объявляется явно, если нужно, чтобы он сработал именно на её границе:

(
    trap 'rm -f /tmp/sub.lock' EXIT
    do_something_that_might_fail
)

Альтернатива — использовать set -E в комбинации с trap '...' ERR, чтобы ERR‑trap наследовался функциями и подоболочками. С EXIT‑trap наследования нет, его нужно ставить вручную внутри каждой подоболочки, где он нужен.

Глобы без совпадений возвращают сами себя

В дефолтном Bash, если глоб не находит ни одного файла, он подставляется в команду как литерал:

for f in /var/log/*.gz; do
    process "$f"
done

Если в /var/log нет ни одного .gz‑файла, цикл выполнится ровно один раз, и $f будет равно строке /var/log/*.gz — чему‑то такому, чего на диске не существует. Команда process получит «файл», который не открывается, и поведение скрипта непредсказуемо.

Лечится через shopt -s nullglob (несовпадающий глоб становится пустым списком) либо failglob (несовпадающий глоб считается ошибкой):

shopt -s nullglob

for f in /var/log/*.gz; do
    process "$f"
done
# если файлов нет, цикл просто не выполнится

nullglob — самое практичное решение для большинства скриптов, и включать его имеет смысл в шапке наряду с set -euo pipefail.

Итого

Bash ведёт себя не так, как ожидает человек, привыкший к языкам с предсказуемой семантикой. Поведение формально документировано в man bash, но прочитать всю эту документацию целиком мало кто способен.

Минимальная защита — шапка скрипта в виде:

#!/bin/bash
set -euo pipefail
shopt -s nullglob
IFS=$'\n\t'

Это закрывает три из пяти описанных проблем. Оставшиеся две — ((x++)) под set -e и pipe в while нужно знать в лицо и отлавливать на ревью, потому что синтаксически они выглядят совершенно нормально.

И отдельно, если скрипт длиннее ста строк или супер важен для деплоя, имеет смысл задуматься, не пора ли переписать его на Python или Go. Bash хорош для тонкой автоматизации (короткие склейки команд, обёртки над утилитами), а для серьёзной логики его пределы видно довольно быстро.


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

Если хотите проверить, насколько уверенно вы ориентируетесь в таких вещах, можно пройти вступительное тестирование по Linux продвинутого уровня. Это быстрый способ оценить текущий уровень и понять, какие темы уже закрыты, а где ещё есть пробелы.

Ещё можно присмотреться к открытым урокам — они бесплатные, проходят вживую, и на них можно познакомиться с преподавателями‑практиками, задать вопросы и посмотреть, как устроен формат обучения:

  • 4 июня, 20:00 — «Продвинутый Bash». Записаться
    Обсудим расширенные возможности Bash как языка программирования: массивы, отладку скриптов, функции и другие практические вещи.

  • 22 июня, 20:00 — «Память в Linux. Cache, swap, dirty pages». Записаться
    Объясним, как работает память в Linux и как читать показатели использования RAM, swap и кэша.

  • 25 июня, 19:00 — День открытых дверей курса «Администратор Linux. Продвинутый уровень». Записаться
    Разберем, зачем Senior‑инженеру глубокое понимание механизмов изоляции, cgroups, namespaces и других системных тем.

А чтобы не пропускать новые открытые уроки, разборы и материалы для IT‑специалистов, можно подписаться на канал OTUS в MAX.