Свеженаписанный скрипт, который отлично запускается командой ./update-backup из родной папки, упрямо не находится, когда вы набираете его имя где-нибудь в /var/log. Оболочка отвечает "command not found", права на исполнение выставлены, файл на месте, а результата нет. Виноват не скрипт, а переменная PATH: оболочка просто не догадывается заглянуть в ту папку, где он лежит. Разберёмся, как система ищет исполняемые файлы, какие конфигурационные файлы читаются при входе в систему и при открытии терминала, и как безболезненно вписать собственные каталоги в этот поиск.

Оболочка ищет команды по списку каталогов

Переменная PATH хранит не одно значение, а целый список папок, разделённых двоеточиями. Когда пользователь набирает ls или git status, bash не сканирует весь диск. Он по очереди заглядывает в каталоги из списка слева направо и запускает первый найденный исполняемый файл с подходящим именем. Вот как выглядит PATH в свежей Ubuntu 26.04:

/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin

Семь каталогов, и порядок здесь решает всё. В /usr/bin живут обычные программы из репозиториев, /sbin и /usr/sbin зарезервированы для системных утилит, доступных администратору, а /usr/local/bin по давней традиции отдаётся софту, который администратор собрал или поставил вручную в обход пакетного менеджера. Если в двух каталогах списка лежат файлы с одинаковым именем, сработает тот, чья папка стоит в списке раньше.

Посмотреть, что именно найдёт оболочка и откуда, позволяют две встроенные команды:

command -v python3
type -a python3

Первая напечатает путь к программе, которая будет запущена, например /usr/bin/python3. Вторая перечислит вообще все совпадения по всем каталогам списка, поэтому она незаменима, когда непонятно, чья версия команды победила. Наглядный пример из жизни: администратор собрал собственную сборку gcc и положил её в /usr/local/bin, после чего type -a показывает два совпадения сразу, и первым идёт именно локальный вариант:

$ type -a gcc
gcc is /usr/local/bin/gcc
gcc is /usr/bin/gcc

Именно поэтому порядок каталогов стоит воспринимать как очередь приоритетов, а не как случайный набор путей. Здесь же проявляется особенность bash: найденные однажды пути кэшируются в хеш-таблице, чтобы не искать заново при каждом вызове. Если вы переместили бинарник в другое место, оболочка ещё некоторое время будет указывать на старый адрес. Лечится это встроенной командой hash -r, которая сбрасывает кэш.

Вход в систему и обычная вкладка терминала читают разные файлы

Самая частая путаница с PATH связана не с переменной, а с тем, какой именно файл конфигурации достанется оболочке при старте. Bash различает два сценария запуска. При входе в систему через консоль, по ssh или через экранный менеджер входа стартует так называемая login-оболочка, и она читает /etc/profile, а затем ищет в домашнем каталоге ~/.bash_profile, ~/.bash_login и ~/.profile именно в этом порядке. Исполняется первый файл, который нашёлся, остальные игнорируются, даже если существуют.

Вкладка терминала внутри графической сессии, окно tmux или встроенный терминал в IDE запускают оболочку уже без "входа", и в этом случае bash читает только ~/.bashrc, не трогая ни /etc/profile, ни ~/.profile. Отсюда классическая ловушка: пользователь вписал свой каталог в ~/.bash_profile, открыл новую вкладку терминала и не увидел эффекта, потому что вкладка попала в другой сценарий запуска. Стандартное лекарство, которое рекомендует и руководство bash, выглядит так, в файле ~/.bash_profile:

if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi

Эта строка заставляет login-оболочку подтянуть содержимое .bashrc, и переменные перестают зависеть от способа запуска. В Debian и Ubuntu файл ~/.profile уже содержит похожий блок, плюс там же лежит автоматическое добавление каталогов ~/.local/bin и ~/bin в PATH, но только если такие каталоги существуют. Ещё два варианта запуска стоит держать в голове. Обычный скрипт, запущенный командой bash имя-файла, не читает ни .bashrc, ни profile, зато берёт путь к файлу инициализации из переменной BASH_ENV. А когда служба sshd выполняет удалённую команду без интерактивного терминала, bash всё-таки читает ~/.bashrc, что часто выручает при настройке окружения для автоматических задач.

Как понять, в каком режиме работает текущая оболочка? Достаточно набрать echo $0: у login-оболочки имя начинается с дефиса и выглядит как -bash, а у обычной вкладки терминала вывод будет просто bash. Надёжнее опираться на встроенную проверку shopt -q login_shell && echo login || echo not-login, которая печатает ответ словами и не зависит от тонкостей с именами.

Файл /etc/profile и каталог profile.d для всех пользователей сразу

Правки в /etc/profile касаются каждого пользователя машины, поэтому файл защищён правами root и требует sudo. В Ubuntu он выглядит компактно: настраивает приглашение командной строки для root и обычных пользователей, подтягивает /etc/bash.bashrc, а дальше выполняет главный трюк. В конце файла стоит цикл, который по очереди подгружает все скрипты с расширением .sh из каталога /etc/profile.d, отбирая их через run-parts с шаблоном:

if [ -d /etc/profile.d ]; then
  for i in $(run-parts --list --regex '^[a-zA-Z0-9_][a-zA-Z0-9._-]*\.sh$' /etc/profile.d); do
    if [ -r $i ]; then
      . $i
    fi
  done
  unset i
fi

Именно через этот механизм в систему встраиваются дополнения от пакетов: в каталоге /etc/profile.d живут bash_completion.sh, apps-bin-path.sh, добавляющий snap-каталоги, flatpak.sh и другие файлы. Логика подсказывает, что сам /etc/profile лучше не редактировать: обновления дистрибутива могут его переписать, а отследить собственные правки среди стандартного кода потом непросто. Для общесистемного PATH достаточно положить в /etc/profile.d короткий файл, например custom-path.sh:

sudo tee /etc/profile.d/custom-path.sh >/dev/null <<'EOF'
export PATH="$PATH:/opt/tools/bin"
EOF

Такой файл переживёт обновления, виден в списке как отдельная единица и удаляется одним движением. Два практических нюанса. Имя файла в profile.d обязано заканчиваться на .sh, иначе шаблон run-parts его просто отбросит, а исполняемый бит не нужен: скрипты из этого каталога подгружаются командой source в текущую оболочку, а не запускаются отдельным процессом. И выводить на экран там ничего нельзя: любое сообщение из файла инициализации всплывёт при каждом входе в систему и испортит работу протоколов вроде scp, которые не рассчитаны на лишний текст в потоке. Минус у подхода тот же, что и у самого /etc/profile: срабатывает он только для login-оболочек, поэтому для bash его обычно дополняют записью в /etc/bash.bashrc, которую bash читает перед интерактивным стартом.

Добавление собственной папки со скриптами в PATH

Практика сводится к четырём коротким действиям. Сначала создаётся каталог под личные утилиты, затем в него попадает сам скрипт, после чего ему выдаются права на исполнение, и в финале каталог вписывается в PATH. Вся последовательность занимает меньше минуты:

mkdir -p ~/scripts
mv update-backup ~/scripts/
chmod +x ~/scripts/update-backup
echo 'export PATH="$HOME/scripts:$PATH"' >> ~/.bashrc
source ~/.bashrc

После этого update-backup запускается из любой точки файловой системы одним именем, без ./ и без полного пути. Быстрая проверка сразу после source выглядит так:

$ command -v update-backup
/home/user/scripts/update-backup
$ update-backup

Если вместо пути оболочка отвечает "not found", каталог в список не попал, и стоит перечитать файл конфигурации либо убедиться, что строка вписана именно в тот файл, который читается при вашем способе запуска. Строка с export заслуживает отдельного разговора по двум причинам. Первая: без export переменная осталась бы локальной для самого bash и не досталась бы ни одному запущенному из него процессу, поэтому это слово обязательно. Вторая: конструкция "$HOME/scripts:$PATH" не заменяет старое значение, а приращивает его, и кавычки здесь не украшение, а защита на случай пробелов в путях.

Важен и порядок склейки. Запись PATH="$HOME/scripts:$PATH" ставит вашу папку в начало, и одноимённый скрипт затмевает системную команду: собственный файл с именем grep окажется вызван вместо /usr/bin/grep. Это удобно для тестирования своих версий утилит и столь же опасно при случайном совпадении имён. Обратный порядок, PATH="$PATH:$HOME/scripts", сохраняет приоритет системных программ, и для большинства сценариев он спокойнее. Разумный компромисс: храните скрипты с уникальными именами и не стесняйтесь проверять конфликт через type -a имя-команды.

Проверка результата и ошибки, после которых ничего не работает

Проверка элементарна. Команда echo $PATH должна показать обновлённый список, а command -v имя-скрипта вывести полный путь к нему. source ~/.bashrc применяет правки к текущей сессии немедленно, хотя иногда проще открыть новую вкладку терминала и тем самым заодно убедиться, что файл читается автоматически. Если эффекта нет, почти всегда виновата одна из типовых ошибок:

  1. строка вписана в ~/.bash_profile, а терминал в графической сессии открывает не login-оболочку и читает только ~/.bashrc;
  2. слово export пропущено, и значение не передаётся дочерним процессам;
  3. значение склеивается без кавычек, и пробел в пути разрывает список на бессмыслицу;
  4. в списке появился пустой элемент из двух двоеточий подряд, который bash трактует как текущий каталог, а это открывает дверь для подмены команд вредоносным файлом из случайной папки;
  5. у скрипта нет права на исполнение или отсутствует строка shebang в первой строке файла;
  6. правка внесена в /etc/profile, но графическая сессия была запущена раньше, и требуется полный выход из системы со входом заново.

Отдельный сюрприз поджидает при работе через sudo. Утилита по умолчанию сбрасывает PATH на фиксированный secure_path, прописанный в /etc/sudoers, на Ubuntu это значение /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin. Поэтому команда, видная обычному пользователю, под sudo может внезапно исчезнуть из зоны поиска. Решается это либо размещением нужных файлов в каталогах из secure_path, либо аккуратной правкой Defaults secure_path в sudoers через visudo.

Общие каталоги, symlink и /etc/environment без правки конфигурации

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

sudo ln -s /opt/tools/bin/deploy-helper /usr/local/bin/deploy-helper

Каталог /usr/local/bin уже входит в PATH по умолчанию у всех пользователей, ссылка не боится обновлений и снимается обычным rm /usr/local/bin/deploy-helper. Для скриптов, которые ставят Python-инструменты, механизм придумал сам дистрибутив: pip при установке пакетов в пользовательский каталог кладёт исполняемые файлы в ~/.local/bin, а стандартный ~/.profile на Debian и Ubuntu добавляет эту папку в PATH автоматически, как только она появится. Именно поэтому утилиты после pip install --user работают сразу, и достаточно один раз выполнить mkdir -p ~/.local/bin, чтобы тот же механизм заработал для ваших собственных скриптов.

Есть и третий уровень, о котором часто забывают. Графические приложения, запущенные из меню, наследуют окружение не от bash, а от сессии, и их PATH формирует файл /etc/environment, который читает модуль pam_env ещё до старта оболочки. Формат там предельно простой, без export и без склейки:

PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin"

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

В сухом остатке система поиска команд оказалась проще, чем кажется на первый взгляд. Bash идёт по списку каталогов из PATH слева направо, список собирается из нескольких файлов в зависимости от способа запуска оболочки, а человеку достаточно запомнить три опорные точки. Общесистемные настройки живут в /etc/profile и каталоге /etc/profile.d, личные в ~/.bashrc с подстраховкой через ~/.profile, и любое изменение PATH требует слова export. Пара минут на настройку, и каждый ваш скрипт начинает отзываться на имя из любой точки файловой системы, как полноправная системная утилита.