TL;DR: Три способа выполнить код — три разных механизма работы с процессами и окружением.
./script.sh — новый процесс (подоболочка), изменения окружения не видны родителю.
. script.sh (sourcing) — выполнение в текущей оболочке, изменения остаются.
exec cmd — замена текущего процесса, возврата нет.
Зачем это знать
Непонимание разницы между этими механизмами — источник классических багов:
«Поставил cd /app в скрипте, но после запуска всё ещё в старой директории» — подоболочка
«Экспортировал переменные в env.sh, но в терминале их нет» — запустили как ./env.sh, а нужен . env.sh
«Docker-контейнер не получает сигналы, не завершается по SIGTERM» — не использовали exec в entrypoint
Подоболочки (subshells)
Подоболочка — новый процесс shell, который получает копию окружения родителя. Любые изменения (переменные, cd, ulimit) исчезают при выходе из подоболочки.
Когда создаётся подоболочка
# 1. Запуск скрипта как команды./script.sh # новый процесс bashbash script.sh # тоже новый процесс# 2. Круглые скобки — явная подоболочка(cd /tmp; tar xf archive.tar)# После выполнения — вы всё ещё в исходной директории# 3. Пайп (каждая часть — подпроцесс)echo "hello" | while read line; do VAR="$line"doneecho "$VAR" # пусто! while работал в подоболочке пайпа
Практическое применение
Подоболочки полезны, когда нужно временное изменение, не затрагивающее основной скрипт:
# Выполнить команду в другой директории, не трогая pwd(cd /var/log && tar czf /tmp/logs.tar.gz *.log)# Временно изменить PATH(PATH=/opt/custom/bin:$PATH; custom_tool --run)# Копирование дерева каталогов с сохранением прав# (быстрее и надёжнее cp -r)tar cf - orig | (cd target; tar xvf -)
Последний пример — приём из книги Уорда: tar архивирует на лету, пайп передаёт в подоболочку, которая сделала cd в целевую директорию и распаковывает. Проверяйте, что target существует и отделён от orig (в скрипте: [ -d target -a ! orig -ef target ]).
Встроенный синтаксис для одноразовых переменных: подоболочка для одной переменной — перебор. Shell позволяет задать переменную прямо перед командой:
PATH=/opt/custom:$PATH custom_tool # без подоболочкиLANG=C sort file.txt # сортировка в C-локали
Sourcing (включение файла)
Оператор . (точка) или source выполняет файл в текущей оболочке. Переменные, функции, изменения cd — всё остаётся после выполнения.
# Два эквивалентных синтаксиса. ./config.sh # POSIX (работает в любом sh)source ./config.sh # Bash-специфичный
Sourcing vs запуск
./script.sh . ./script.sh
│ │
fork() (нет fork)
│ │
Новый bash Текущий bash
читает script.sh читает script.sh
│ │
exit Продолжает работу
│ │
Изменения потеряны Изменения СОХРАНЕНЫ
Это не то же самое, что выполнение скрипта как команды: при запуске ./script.sh он работает в новой оболочке и не отдаёт ничего, кроме вывода и кода возврата.
Осторожно: sourcing выполняет все команды файла в вашей оболочке. exit внутри sourced-файла завершит вашу оболочку. Поэтому в sourced-файлах определяют только переменные и функции, избегая exit и побочных эффектов.
exec — замена процесса
exec — встроенная команда shell, которая заменяет текущий процесс оболочки на указанную программу (через системный вызов exec()). Возврата нет — shell перестаёт существовать. Смысл — экономия ресурсов: один процесс вместо двух.
# В терминале:exec cat # shell заменён на cat# Ctrl+D → терминал закроется (shell больше нет)# В скрипте:#!/bin/bashecho "Preparing..."exec /usr/bin/myapp --flagecho "This line NEVER executes"
Docker entrypoint — главный use case
#!/bin/bash# entrypoint.shexport DB_URL="postgres://${DB_HOST}:${DB_PORT}/app"# Заменить shell на приложение — оно становится PID 1# и получает сигналы Docker (SIGTERM при docker stop)exec "$@"
Без exec shell остаётся PID 1, перехватывает SIGTERM, а приложение его не получает — контейнер зависает на docker stop (10 сек ожидания → SIGKILL).
exec для перенаправления
Менее известное применение — перенаправление файловых дескрипторов для всего скрипта:
# Весь stdout скрипта → в файлexec > /var/log/myscript.log 2>&1echo "Это пойдёт в файл, не в терминал"
Подстановка команд: $() — подводные камни
$() запускает команду в подоболочке и подставляет её stdout. Внутри можно использовать пайпы, кавычки и вложенные подстановки:
FLAGS=$(grep ^flags /proc/cpuinfo | sed 's/.*://' | head -1)DATE=$(date +%Y-%m-%d)
Чего НЕ делать
# ❌ $(ls) — бессмысленно и ломается на пробелахfor f in $(ls *.txt); do ... # если файл "my file.txt" → две итерацииfor f in *.txt; do ... # ✅ shell glob — безопасно и быстро# ❌ $(find ...) — ломается на пробелахfiles=$(find . -name '*.log')rm $files # ОПАСНОfind . -name '*.log' -print0 | xargs -0 rm # ✅ безопасноfind . -name '*.log' -exec rm {} + # ✅ без xargs