Скрипт меняет каталог и устанавливает переменную, всё внутри отрабатывает верно - а после возврата в оболочку оказывается, что каталог прежний, а переменной нет. Знакомое недоумение. Виновата не ошибка в скрипте, а способ его запуска. В 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 "сюда мы уже не попадём" # эта строка не выполнится
После команды замещения всё, что написано ниже, не исполнится никогда, потому что прежнего процесса больше нет - его место занял запущенный скрипт. Когда тот завершится, завершится и сам сеанс, ведь возвращаться некуда. Это поведение полезно в особых случаях: например, скрипт-обёртка настраивает окружение, а затем замещает себя основной программой, чтобы не плодить лишний процесс-посредник в дереве процессов.
Команда замещения умеет и другое - перенаправлять потоки текущей оболочки, не запуская ничего. Но в контексте запуска скриптов её суть именно в безвозвратной замене процесса. Применять её к обычному скрипту, ожидая вернуться в оболочку, - ошибка: возврата не будет.
Сводная картина области видимости решает выбор
Три способа выстраиваются в ясную таблицу различий, и проще всего держать их в голове через судьбу переменных и процесса.
Отдельный запуск порождает дочернюю подоболочку с собственным идентификатором процесса. Скрипт наследует окружение родителя, но все его изменения - переменные, смена каталога, новые функции - живут лишь в подоболочке и гибнут с её завершением. Родительская оболочка остаётся нетронутой. Это режим изоляции, выбор для подавляющего большинства обычных скриптов.
Подключение выполняет скрипт в текущей оболочке без нового процесса. Все изменения вступают в силу прямо здесь и переживают завершение скрипта. Это режим влияния на текущее окружение, выбор для файлов настроек, библиотек функций и конфигураций, которые должны изменить текущий сеанс.
Замещение заменяет текущий процесс скриптом безвозвратно. Возврата в исходную оболочку нет, после завершения скрипта сеанс заканчивается. Это режим передачи эстафеты, выбор для обёрток, которые настраивают окружение и уступают место основной программе.
Практическое правило простое. Нужно, чтобы скрипт изменил текущую оболочку - установил переменные, сменил каталог, определил функции для дальнейшего использования - подключают его командой подключения. Нужно просто выполнить работу, не трогая текущее окружение, - запускают отдельным процессом. Нужно заместить процесс без возврата ради чистоты дерева процессов - применяют замещение.
Главная мысль одна: выбор способа запуска - это выбор области видимости. Потеря переменных после запуска скрипта почти всегда означает, что его запустили отдельным процессом там, где требовалось подключение. Стоит запомнить связку - подоболочка теряет изменения, подключение их сохраняет, замещение не возвращается - и загадочное исчезновение переменных и несменившийся каталог перестают удивлять. Скрипт меняет окружение ровно настолько, насколько это позволяет выбранный способ его запуска.