Скрипт меняет каталог и устанавливает переменную, всё внутри отрабатывает верно - а после возврата в оболочку оказывается, что каталог прежний, а переменной нет. Знакомое недоумение. Виновата не ошибка в скрипте, а способ его запуска. В bash есть три принципиально разных пути выполнить скрипт, и они по-разному обходятся с переменными, каталогом и самим процессом. Один запускает скрипт в отдельной оболочке, изолированной от текущей. Второй выполняет его прямо в текущей оболочке, меняя её окружение. Третий и вовсе замещает текущий процесс скриптом без возврата назад.

Разница между ними - не тонкость для педантов, а ключ к тому, почему файлы настроек оболочки подключают одним способом, а обычные скрипты запускают другим. Понимание трёх режимов объясняет и потерю переменных, и необходимость подключать измененный файл настроек, и редкие случаи, когда процесс нужно заместить целиком. Разберём каждый способ через призму области видимости переменных.

Отдельный запуск порождает подоболочку, и изменения исчезают

Самый привычный способ - запустить скрипт как отдельную команду, указав путь к нему или вызвав через имя оболочки. При этом порождается новый процесс - подоболочка, дочерняя по отношению к текущей. Скрипт выполняется в ней, и все его изменения остаются внутри этой дочерней оболочки.

./скрипт.sh          # запуск по пути, нужны права на исполнение
bash скрипт.sh       # запуск через имя оболочки, права не нужны

Возьмём простой скрипт, который ставит переменную и меняет каталог.

#!/bin/bash
сообщение="Привет"
echo "$сообщение"
cd /var/log

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

# До запуска переменной нет, каталог домашний
./скрипт.sh
# После: переменной по-прежнему нет, каталог не сменился

Это поведение - не баг, а изоляция. Скрипт работает в своём пузыре, не трогая окружение запустившего. Большинству скриптов именно это и нужно: сделать свою работу и не наследить в родительской оболочке. Убедиться в отдельности процесса легко - идентификатор процесса у запущенного скрипта свой, отличный от текущей оболочки, а родительским значится текущая оболочка.

Запуск по пути требует прав на исполнение у файла и указания пути с точкой и косой чертой впереди, потому что текущий каталог обычно не входит в список поиска команд. Запуск через явное имя оболочки прав на исполнение не требует - оболочка просто читает файл как набор команд.

Подключение через source выполняет скрипт в текущей оболочке

Второй способ переворачивает картину. Команда подключения - словом или равнозначной ей точкой - выполняет содержимое файла прямо в текущей оболочке, без порождения подоболочки. Никакого нового процесса не возникает, команды исполняются так, будто их набрали вручную в текущем сеансе.

source скрипт.sh     # подключение словом
. скрипт.sh          # то же самое точкой, официальный вариант POSIX

Тот же скрипт, подключённый таким образом, оставляет следы. Переменная сообщения после подключения доступна в текущей оболочке, а каталог действительно сменился на каталог журналов. Изменения вступили в силу здесь, потому что выполнялись здесь же.

source скрипт.sh
echo "$сообщение"    # Привет - переменная доступна
pwd                  # /var/log - каталог сменился

Идентификатор процесса при подключении не меняется - это та же оболочка, тот же процесс. Подключению не нужны права на исполнение у файла, ведь он не запускается как программа, а читается текущей оболочкой построчно. Файл должен быть лишь корректным набором команд оболочки.

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

Команда exec замещает текущий процесс без возврата

Третий способ стоит особняком и встречается реже. Команда замещения процесса не порождает подоболочку и не выполняет скрипт в текущей оболочке - она заменяет текущий процесс оболочки запускаемым, целиком. Текущая оболочка прекращает существование, на её месте оказывается новый процесс, и управление обратно не возвращается.

exec ./скрипт.sh
echo "сюда мы уже не попадём"   # эта строка не выполнится

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

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

Сводная картина области видимости решает выбор

Три способа выстраиваются в ясную таблицу различий, и проще всего держать их в голове через судьбу переменных и процесса.

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

Подключение выполняет скрипт в текущей оболочке без нового процесса. Все изменения вступают в силу прямо здесь и переживают завершение скрипта. Это режим влияния на текущее окружение, выбор для файлов настроек, библиотек функций и конфигураций, которые должны изменить текущий сеанс.

Замещение заменяет текущий процесс скриптом безвозвратно. Возврата в исходную оболочку нет, после завершения скрипта сеанс заканчивается. Это режим передачи эстафеты, выбор для обёрток, которые настраивают окружение и уступают место основной программе.

Практическое правило простое. Нужно, чтобы скрипт изменил текущую оболочку - установил переменные, сменил каталог, определил функции для дальнейшего использования - подключают его командой подключения. Нужно просто выполнить работу, не трогая текущее окружение, - запускают отдельным процессом. Нужно заместить процесс без возврата ради чистоты дерева процессов - применяют замещение.

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