Переменные окружения в Linux: export, PATH, .env и Python

опубликовано
читать
≈ 6 мин
автор
среда и версия
Debian 12, bash 5.2, sudo 1.9, Python 3.11, python-dotenv 1.2
содержание пройти тест Linux

Переменная окружения — пара «имя=значение», которую процесс получает при запуске и передаёт своим дочерним процессам. Посмотреть её можно командой printenv ИМЯ или echo $ИМЯ, задать — через export ИМЯ=значение, а чтобы она пережила закрытие терминала, строку с export дописывают в ~/.bashrc. Ниже всё это на живом выводе Debian 12, плюс PATH, sudo и чтение файла .env из Python.

Linux / 01

Кто видит переменную

Кто видит переменнуюname=Anna без export видна только текущей оболочке, дочерний процесс получает пустоту. export CITY=Kazan видна оболочке и дочерним процессам, но не новому терминалу. LIMIT=500 перед командой получает только эта команда. Строку export в ~/.bashrc видят текущая оболочка после source, дочерние процессы и каждый новый терминал.как заданатекущая оболочкадочерний процессновый терминалname=AnnaAnnaпустопустоexport CITY=KazanKazanKazanпустоLIMIT=500 командапусто500пустов ~/.bashrcпосле sourceдада
Без export переменная остаётся внутри оболочки, с export переходит к дочерним процессам, а в новый терминал попадает, только если записана в ~/.bashrc.

Как посмотреть переменные окружения

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 вопросов, баллы видны без регистрации.

Источники