Переменная окружения — пара «имя=значение», которую процесс получает при запуске и передаёт своим дочерним процессам. Посмотреть её можно командой printenv ИМЯ или echo $ИМЯ, задать — через export ИМЯ=значение, а чтобы она пережила закрытие терминала, строку с export дописывают в ~/.bashrc. Ниже всё это на живом выводе Debian 12, плюс PATH, sudo и чтение файла .env из Python.
Кто видит переменную
Как посмотреть переменные окружения
printenv без аргументов выводит всё окружение, с именами — только нужные значения:
printenv HOME USER SHELL
/home/dev
dev
/bin/bash
Значение одной переменной удобно подставить в команду через $. Самая важная из них — PATH, о ней ниже:
echo $PATH
/home/dev/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
Регистр в именах важен: PATH и path — разные переменные. Переменные окружения по традиции пишут заглавными буквами.
Обычная переменная и export
Запись name=Anna создаёт переменную только в текущей оболочке. Любая программа, которую ты из неё запустишь, этой переменной не получит:
name=Anna
echo "в оболочке: $name"
bash -c 'echo "в дочернем процессе: $name"'
в оболочке: Anna
в дочернем процессе:
export делает переменную частью окружения, и дальше её наследуют все дочерние процессы — скрипты, python, npm:
export CITY=Kazan
bash -c 'echo "в дочернем процессе: $CITY"'
в дочернем процессе: Kazan
Если переменная нужна одной команде, её пишут прямо перед ней. Команда её получит, а оболочка — нет:
LIMIT=500 bash -c 'echo "limit для команды: $LIMIT"'
echo "limit после команды: $LIMIT"
limit для команды: 500
limit после команды:
Удаляет переменную unset:
unset CITY
echo "после unset: $CITY"
после unset:
PATH: где shell ищет команды
Когда ты пишешь имя команды без пути, bash перебирает каталоги из PATH слева направо и запускает первый найденный файл. Скрипт из каталога ~/tools, которого в PATH нет, по имени не запустится:
report.sh
command -v report.sh
echo $?
-bash: report.sh: command not found
1
Каталог добавляют в начало, сохраняя старое значение через $PATH:
export PATH="$HOME/tools:$PATH"
command -v report.sh
report.sh
/home/dev/tools/report.sh
report: ok
Без :$PATH в конце новое значение заменит весь список, и пропадут даже стандартные команды:
PATH=/home/dev/bin
ls
-bash: ls: command not found
Это лечится одной строкой с нормальным значением, например export PATH=/usr/local/bin:/usr/bin:/bin, или новым терминалом: испорченный PATH живёт только в текущей оболочке.
Как сохранить переменную надолго
export действует до закрытия терминала. Чтобы переменная появлялась в каждой новой оболочке, строку с ней дописывают в ~/.bashrc:
echo 'export CITY=Kazan' >> ~/.bashrc
bash -i -c 'echo "новая оболочка: $CITY"'
новая оболочка: Kazan
Уже открытый терминал файл сам не перечитает: выполни source ~/.bashrc или открой новый. ~/.bashrc bash читает при каждом запуске интерактивной оболочки, а ~/.profile — при входе в систему, в том числе по SSH. В Debian и Ubuntu ~/.profile сам подключает ~/.bashrc, поэтому строки из него работают и там. Удалить сохранённую переменную — значит убрать строку из файла и выполнить unset.
Главная мысль:
name=значение— только для текущей оболочки,export— для неё и запущенных из неё программ, строка в~/.bashrc— для каждого нового терминала.
sudo, cron и службы видят своё окружение
sudo по умолчанию очищает окружение: так настроен Debian, строка Defaults env_reset в /etc/sudoers. Переменная, которую ты экспортировал, до команды под sudo не доходит, и printenv завершается с кодом 1:
export REPORT_LIMIT=500
sudo printenv REPORT_LIMIT
echo $?
1
Передать конкретную переменную можно явно, если правила sudo это разрешают:
sudo --preserve-env=REPORT_LIMIT printenv REPORT_LIMIT
500
PATH под sudo тоже свой — из настройки secure_path. Поэтому скрипт, который запускается по имени из твоего ~/tools, под sudo не находится:
sudo printenv PATH
sudo report.sh
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
sudo: report.sh: command not found
Под sudo такие скрипты запускают по полному пути. Похожая история с расписанием и службами: задача cron получает короткий PATH и почти пустое окружение, а службе systemd переменные задают строкой Environment= в unit-файле.
Файл .env и переменные в Python
Настройки приложения — адрес базы, ключи, режим отладки — удобно держать вне кода, в переменных окружения. Для разработки их складывают в файл .env рядом с проектом:
DATABASE_URL=postgresql://app:app_password@localhost:5432/shop
DEBUG=1
Python читает окружение через os.environ. Обращение по ключу падает, если переменной нет, а os.getenv возвращает значение по умолчанию:
import os
database_url = os.environ["DATABASE_URL"]
debug = os.getenv("DEBUG", "0") == "1"
print(f"database: {database_url}")
print(f"debug: {debug}")
Сам по себе файл .env ничего не делает, и без него программа падает на первой строке:
python3 settings.py
Traceback (most recent call last):
File "/home/dev/app/settings.py", line 3, in <module>
database_url = os.environ["DATABASE_URL"]
~~~~~~~~~~^^^^^^^^^^^^^^^^
File "<frozen os>", line 679, in __getitem__
KeyError: 'DATABASE_URL'
Загрузить файл в оболочку можно так: set -a экспортирует всё, что задаётся следом, source выполняет файл, set +a выключает режим:
set -a; source .env; set +a
python3 settings.py
database: postgresql://app:app_password@localhost:5432/shop
debug: True
source выполняет .env как код на bash, поэтому чужие файлы так не загружают. Внутри Python удобнее библиотека python-dotenv: она разбирает файл, ничего не выполняя. Ставят её в виртуальное окружение командой pip install python-dotenv:
import os
from dotenv import load_dotenv
load_dotenv()
print("database:", os.environ["DATABASE_URL"])
print("debug:", os.getenv("DEBUG"))
.venv/bin/python settings_dotenv.py
DEBUG=0 .venv/bin/python settings_dotenv.py
database: postgresql://app:app_password@localhost:5432/shop
debug: 1
database: postgresql://app:app_password@localhost:5432/shop
debug: 0
Во втором запуске победил DEBUG=0 из окружения: по умолчанию load_dotenv() не перезаписывает уже заданные переменные. Чтобы файл был главнее, есть load_dotenv(override=True). Сам .env с паролями в репозиторий не кладут — его добавляют в .gitignore, а в репозиторий кладут пример без секретов, например .env.example.
Ошибки, на которых спотыкаются
Пробелы вокруг знака =. export CITY = Kazan bash читает как три слова, и = для него — неверное имя:
-bash: export: `=': not a valid identifier
command not found после правки PATH. Новое значение заменило весь список. Добавляй каталог через "$HOME/tools:$PATH", а испорченную сессию закрой.
Программа не видит переменную. Её задали без export, поэтому она осталась в оболочке. Проверка — bash -c 'echo $ИМЯ'.
Переменная пропала в новом терминале. export действует до закрытия окна. Запиши строку в ~/.bashrc.
Под sudo переменной нет. sudo очищает окружение. Передай её через --preserve-env=ИМЯ или задай прямо в команде, которую запускаешь.
KeyError в Python. Переменную не загрузили: .env не прочитан или программа запущена не из того окружения. Для необязательных настроек используй os.getenv со значением по умолчанию.
Значение из .env не применилось. Переменная с тем же именем уже есть в окружении, и load_dotenv() её не трогает. Проверь printenv ИМЯ или передай override=True.
Частые вопросы
Как посмотреть все переменные окружения
printenv или env без аргументов. Найти нужную среди них помогает grep: printenv | grep -i proxy. Как работает |, разобрано в материале о конвейерах.
Чем ~/.bashrc отличается от ~/.profile
~/.profile выполняется один раз при входе в систему, ~/.bashrc — при запуске каждой интерактивной оболочки bash. В Debian и Ubuntu первый подключает второй, поэтому переменные для своего пользователя обычно пишут в ~/.bashrc.
Как задать переменную в скрипте
Так же, через export, но она пропадёт вместе со скриптом: дочерний процесс не может изменить окружение родителя. Чтобы скрипт поменял переменные текущего терминала, его запускают через source. Подробнее о скриптах — в разборе bash-скриптов.
Где потренироваться
Конфигурация и секреты — тема в карте навыков DevOps, рядом со скриптами и службами. Проверить себя по Linux можно в тесте навыков: 8 вопросов, баллы видны без регистрации.