Что такое…

Что такое подоболочка (subshell) — и почему счётчик в цикле снова 0

Иллюстрация: под стеклянным колпаком крошечная копия кухни, где щёлкает счётчик, а настоящий счётчик снаружи стоит на нуле

Вот скрипт на четыре строки. Он считает строки в файле:

count=0
cat todo.txt | while read line; do
  count=$((count + 1))
done
echo "$count"

В файле пять строк. Цикл честно проходит все пять. А echo печатает 0.

Ошибки нет. Опечатки нет. Просто цикл работал в другой комнате. Эта комната называется подоболочка, по-английски subshell. Разберёмся, где она появляется — и почему ты встречаешь её каждый день, даже не зная слова.

Что такое подоболочка

Подоболочка — это копия твоей оболочки, запущенная отдельным процессом. Она получает всё, что было у родителя: переменные, текущую папку, функции.

Но связь односторонняя. Копия видит родителя. Родитель копию — нет.

Всё, что подоболочка поменяет у себя, исчезнет вместе с ней. Новая переменная, cd в другую папку, export — всё остаётся внутри.

Представь фотокопию договора. Ты можешь исписать копию пометками. Оригинал от этого не изменится.

Где она появляется сама

Ты почти никогда не пишешь «создай подоболочку». Bash делает это сам в четырёх местах:

  1. Круглые скобки ( ... ). Всё внутри идёт в копии.
  2. Подстановка команды $(...). Например, today=$(date +%F). Команда внутри работает в копии, наружу возвращается только её вывод.
  3. Пайп |. В bash каждая часть пайпа — отдельный процесс. Включая последнюю.
  4. Фон &. Команда в фоне — тоже копия.

Третий пункт и убил наш счётчик. while стоял после |, значит, жил в копии. Копия насчитала пять и тихо исчезла.

Проверить, где ты сейчас, можно переменной $BASH_SUBSHELL:

echo $BASH_SUBSHELL        # 0 — ты в основной оболочке
(echo $BASH_SUBSHELL)      # 1 — внутри скобок

Смотри, хитрая деталь: в zsh (оболочка по умолчанию на macOS) тот же скрипт напечатает 5. Zsh запускает последнюю часть пайпа прямо в текущей оболочке. Поэтому скрипт «работает у меня» и ломается на сервере, где bash.

Как починить счётчик

Убери пайп. Пусть while читает файл сам:

count=0
while read line; do
  count=$((count + 1))
done < todo.txt
echo "$count"   # 5

Знак < подаёт файл на вход циклу без нового процесса. Цикл живёт в основной оболочке — и счётчик выживает.

Если данные приходят из команды, а не из файла, есть второй приём:

while read line; do
  count=$((count + 1))
done < <(git log --oneline)

Конструкция < <(...) запускает в копии только git log. Сам цикл остаётся дома.

Та же ловушка прячется в exit. Смотри:

cat servers.txt | while read host; do
  ping -c1 "$host" || exit 1
done
echo "Все сервера живы"

Сервер упал, exit 1 сработал. А скрипт всё равно печатает «Все сервера живы». exit закрыл только копию, в которой жил цикл. Основной скрипт даже не заметил. Лечится тем же способом: убери пайп, подай файл через <. И проверяй код выхода после цикла, если от него зависит дальнейшая работа.

Когда копия — это подарок

Подоболочка не только ломает. Иногда она именно то, что нужно.

Нужно зайти в папку, сделать дело и вернуться? Скобки делают это без cd -:

(cd frontend && pnpm build)
# ты по-прежнему в корне проекта

Даже если сборка упадёт посередине, ты не застрянешь в frontend. Копия умерла — твоя папка не менялась.

То же с переменными. Хочешь запустить одну команду с другими настройками и не испортить терминал:

(export NODE_ENV=production; node server.js)

После скобок NODE_ENV снова прежняя. Про сами переменные окружения — отдельный разбор.

Зачем это тебе, если код пишет ИИ

Агенты вроде Claude Code и Codex запускают команды через терминал. И часто — каждую команду в свежей оболочке.

Поэтому агент делает cd backend одной командой, а следующая снова стартует в корне. Он делает source .venv/bin/activate, а через шаг Python опять не тот. Это та же логика копии, только масштабом крупнее. Подробно про source — в статье source или ./script.sh.

Рабочий приём: проси агента склеивать связанные шаги в одну команду. Например, cd backend && pnpm test. Тогда папка и переменные живут столько, сколько нужно.

Подоболочка — это то же, что новый терминал?

Нет. Новое окно терминала начинает с чистого листа и читает настройки заново. Подоболочка наследует всё текущее: переменные, функции, папку.

Фигурные скобки тоже создают подоболочку?

Нет. Группа вида { cd app; ls; } выполняется в текущей оболочке, и изменения сохраняются. Нужна изоляция — круглые скобки, нужен общий результат — фигурные.

Подоболочка замедляет скрипт?

Каждая копия — это новый процесс, и он стоит пару миллисекунд. В цикле на тысячи итераций с $(...) внутри это заметно. В обычных скриптах — нет.

Учись вайб-кодингу, а не просто читай о нём

Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.

Открыть приложение
Робот KODiQ

ИИ-редактор KODiQ. Пишет про вайб-кодинг и AI-инструменты простым языком — каждый день.

Все статьи →