Абсолютные и относительные пути
«У меня скрипт не находит файл, хотя я его точно создал!» — каждый Junior хоть раз попадал в эту ловушку. Причина почти всегда — путаница между абсолютным и относительным путём. В этом уроке разберём, что есть что, и научимся не путаться.
Абсолютный vs относительный
Абсолютный путь начинается с /. Это полный путь от корня файловой системы:
/home/user/projects/etl/main.py
/etc/passwd
/var/log/syslog
Относительный путь не начинается с /. Он отсчитывается от текущей директории ($PWD):
main.py # файл в текущей папке
./main.py # то же самое, явный './'
subfolder/main.py # в подпапке
../other-project/main.py # на уровень вверх и в другую папку
Простой тест: «работает ли путь, если изменить cd?»
- Абсолютный — да, всегда указывает на одно и то же.
- Относительный — нет, после
cdтот же путь укажет на другое.
$ pwd
/home/user/projects
$ ls main.py # относительный — ищет /home/user/projects/main.py
$ ls /home/user/projects/main.py # абсолютный — то же самое
$ cd /tmp
$ ls main.py # ищет /tmp/main.py — НЕ ТУ ЖЕ!
$ ls /home/user/projects/main.py # ВСЁ ЕЩЁ работает
./ — explicit current
./file означает «file в текущей папке». Зачем ./ если можно просто file? Две причины:
- Для исполнения скриптов. Когда вы хотите запустить
./script.sh, нужно явное./— иначе bash ищет в PATH:
$ ls
script.sh
$ script.sh # ищет в PATH — не найдёт
bash: script.sh: command not found
$ ./script.sh # явный путь — запустит
Hello from script
- Когда имя файла начинается с
-. Без./команда подумает, что это опция:
$ touch -- -weird-name.txt # создать файл с именем '-weird-name.txt'
$ rm -weird-name.txt # ОПАСНО: rm подумает, что -w -e -i -r... опции
rm: invalid option -- 'w'
$ rm ./-weird-name.txt # сработает корректно
.. — родительская директория
.. всегда указывает на родительскую папку:
$ pwd
/home/user/projects/airflow
$ cd ..
$ pwd
/home/user/projects
$ cd ../..
$ pwd
/home
$ cd ../user/projects/airflow
$ pwd
/home/user/projects/airflow
В каждой директории есть . (текущая) и .. (родительская) как реальные directory entries:
$ ls -la | head -3
total 24
drwxr-xr-x 3 user user 4096 May 13 14:00 .
drwxr-xr-x 47 user user 4096 May 13 13:55 ..
Это часть файловой системы. Когда ядро резолвит путь ../foo/bar, оно идёт по этим entries.
Исключение — корень: /.. равно /. Дальше идти некуда.
Tilde ~ — home
~ — это сокращение, которое shell разворачивает в значение $HOME. Обычно $HOME — это /home/user/ (на Linux) или /Users/user/ (на macOS).
$ echo $HOME
/home/user
$ echo ~
/home/user
$ cd ~/projects # cd /home/user/projects
$ ls ~/Documents # ls /home/user/Documents
Важно: tilde разворачивается только bash-ом перед запуском команды. Программы сами не знают про ~:
$ python -c "import os; print(os.path.exists('~/projects'))"
False # Python не разворачивает ~
$ python -c "import os; print(os.path.expanduser('~/projects'))"
/home/user/projects # Python должен явно expand
В bash:
$ FILE="~/projects/main.py"
$ ls $FILE
ls: cannot access '~/projects/main.py': No such file or directory
# tilde не разворачивается в переменных в кавычках!
$ FILE="$HOME/projects/main.py"
$ ls $FILE
# работает
Поэтому в скриптах лучше использовать $HOME, а не ~ в строках.
~user — home другого пользователя
~alice — это home директория пользователя alice (если такой пользователь есть):
$ ls ~alice
# содержимое /home/alice/
$ cd ~root
$ pwd
/root
Это разворачивается из /etc/passwd (где указано, где home каждого пользователя). Подробнее в модуле 6.
OLDPWD, $PWD
Переменные окружения, которые полезно знать:
$ cd /var/log
$ echo $PWD
/var/log
$ cd /etc
$ echo $OLDPWD
/var/log
$ cd $OLDPWD
$ pwd
/var/log
realpath — резолв всех symlinks
realpath (или readlink -f) — это команда, которая «разворачивает» путь:
- резолвит все symlinks (показывает реальный путь);
- удаляет
..,.; - получает абсолютный путь.
# Допустим, /home/user — symlink на /mnt/users/user
$ realpath ~
/mnt/users/user
$ realpath ./relative/path
/home/user/current/relative/path
$ realpath /etc/alternatives/python3
/usr/bin/python3.13
Это часто нужно в скриптах для нормализации:
SCRIPT_DIR=$(realpath "$(dirname "${BASH_SOURCE[0]}")")
echo "Script lives in: $SCRIPT_DIR"
Эта конструкция гарантированно даст абсолютный путь к директории, где лежит сам скрипт. Полезно, когда скрипт хочет открыть файлы относительно своего расположения.
readlink без -f показывает только цель symlink (не резолвит цепочки). readlink -f — резолвит всё. На macOS readlink -f нет (BSD-вариант), используйте realpath. Или ставьте coreutils через brew, тогда greadlink -f работает.
Канонический путь в реальных задачах
Допустим, у вас DAG в Airflow:
# /opt/airflow/dags/etl_dag.py
def my_task():
data_path = "../../data/raw.csv"
process(data_path)
В какой момент это резолвится? Зависит от того, где запускается код. Если Airflow worker запускается из /opt/airflow, то ../../data/ — это /data/. Если из /, то ../../data/ — это ../../data/… что?
Лучшая практика — НЕ использовать относительные пути в production-коде:
import os
# Берём абсолютный путь из переменной окружения или конфига
DATA_ROOT = os.environ.get("DATA_ROOT", "/var/data")
data_path = os.path.join(DATA_ROOT, "raw.csv")
process(data_path)
То же в bash:
# В начале скрипта — определить абсолютные пути
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
DATA_DIR="${DATA_DIR:-/var/data}"
LOG_DIR="${LOG_DIR:-/var/log/myapp}"
# Дальше работаем только с абсолютами
process "$DATA_DIR/raw.csv" >> "$LOG_DIR/etl.log"
Когда относительные OK
В скриптах относительные пути плохи. Но в интерактивной работе — нормально:
$ cd ~/projects/etl
$ vim main.py # относительный — OK, работает
$ python main.py
$ ls outputs/
В Git тоже относительные — git add ./src/main.py (относительно корня репо).
И в коротких pipelines:
$ cd /var/log
$ grep ERROR syslog | wc -l
Правило: интерактив — относительные OK; скрипты production — абсолютные.
Проверка существования путей
В bash есть условия для проверки:
# Файл существует?
$ [[ -e /etc/passwd ]] && echo "exists"
exists
# Существует и это файл (не папка)?
$ [[ -f /etc/passwd ]] && echo "is file"
is file
# Существует и это папка?
$ [[ -d /etc ]] && echo "is dir"
is dir
# Существует и это symlink?
$ [[ -L /etc/alternatives/python ]] && echo "is symlink"
# Можно читать?
$ [[ -r /etc/shadow ]] || echo "cannot read"
cannot read
# Можно писать?
$ [[ -w /tmp ]] && echo "can write"
can write
Это всё в скриптах для проверки перед операцией. Подробнее в модуле 17.
Попробуй сам
Создайте sandbox:
$ mkdir -p ~/linux-sandbox/lesson-paths/{src,data,logs}
$ cd ~/linux-sandbox/lesson-paths
# Абсолютные vs относительные
$ pwd
/home/user/linux-sandbox/lesson-paths
$ ls src/
$ ls /home/user/linux-sandbox/lesson-paths/src/
# Одинаково
$ cd src
$ ls ../data/
$ ls /home/user/linux-sandbox/lesson-paths/data/
# Одинаково
Поиграйте с tilde:
$ echo "I am at: ~/some/path" # tilde НЕ развернётся в строке
I am at: ~/some/path
$ echo "I am at: $HOME/some/path" # развернётся
I am at: /home/user/some/path
$ FILE=~/some/path # развернётся (без кавычек!)
$ echo $FILE
/home/user/some/path
$ FILE="~/some/path" # в кавычках — нет
$ echo $FILE
~/some/path
Symlinks:
$ ln -s /etc/passwd ./pass-link
$ ls -l pass-link
lrwxrwxrwx 1 user user 11 May 13 14:00 pass-link -> /etc/passwd
$ realpath pass-link
/etc/passwd
$ readlink pass-link
/etc/passwd
$ readlink -f pass-link # резолвит все цепочки
/etc/passwd
realpath всегда даёт реальный конечный путь. Это надёжнее readlink в скриптах.