Файлы запуска Zsh: .zprofile и .zshrc в macOS и Linux

Автоматический перевод Эта статья была автоматически переведена с оригинальной английской версии.

Если терминал работает медленно или переменная окружения появляется в одном терминале, но отсутствует в другом, причина часто связана с режимом запуска Zsh. Чаще всего используют файлы ~/.zprofile и ~/.zshrc, однако само имя файла не говорит, какую именно строку следует туда помещать.

Zsh выбирает файлы запуска по двум независимым признакам: является ли shell login-shell и является ли он интерактивным. Терминальные приложения в macOS и Linux могут выбирать разные сочетания этих режимов, поэтому проверяйте запущенный shell, а не предполагайте, что всё определяется операционной системой.

Коротко. Помещайте инициализацию login-сессии в .zprofile, а интерактивное поведение — в .zshrc. Если установлена RCS, файл .zshenv должен оставаться тихим и минимальным. Не переносите интерактивные хуки менеджеров версий в .zprofile и не подключайте весь profile из .zshrc; изучите сгенерированный код инструмента и следуйте его актуальным инструкциям для Zsh.

Zsh проверяет два независимых флага

Файлы запуска Zsh, выбираемые по login- и интерактивному режимамФайлы запуска Zsh, выбираемые по login- и интерактивному режимам

Shell может быть:

  • login и интерактивным, если Apple Terminal настроен на запуск аккаунта с Default login shell
  • не-login и интерактивным — например, это вложенный zsh или один из распространённых вариантов запуска в Linux-терминалах
  • login и неинтерактивным — например, zsh -lc 'command'
  • не-login и неинтерактивным — например, zsh -c 'command'

Терминальные приложения, IDE, удалённые сессии, мультиплексоры и явные флаги shell могут выбирать разные режимы, поэтому одна только операционная система не определяет результат.

Попросите запущенный shell сообщить режим, а не гадайте:

print -r -- "interactive=$options[interactive] login=$options[login]"

Фактический порядок запуска

Сначала Zsh читает глобальные файлы, а затем соответствующий пользовательский файл. При стандартных RCS options порядок пользовательских файлов такой:

  1. $ZDOTDIR/.zshenv — при каждом запуске Zsh
  2. $ZDOTDIR/.zprofile — если shell является login-shell
  3. $ZDOTDIR/.zshrc — если shell интерактивный
  4. $ZDOTDIR/.zlogin — если shell является login-shell
  5. $ZDOTDIR/.zlogout — при штатном завершении login-shell

Если ZDOTDIR не установлена, Zsh использует $HOME. Расположение общесистемных файлов зависит от конкретной установки; часто используются /etc/zshenv, /etc/zprofile, /etc/zshrc, /etc/zlogin и /etc/zlogout (руководство Zsh).

zsh -f запускается с unset-значением RCS, поэтому обычно пропускает перечисленные выше пользовательские файлы $ZDOTDIR; файл /etc/zshenv читается всегда и может изменить эту опцию.

Последовательность запуска Zsh для четырёх режимов shellПоследовательность запуска Zsh для четырёх режимов shell

.zprofile выполняется один раз для каждого процесса login-shell, а не один раз за вход пользователя в компьютер. Если каждая новая вкладка терминала запускает login-shell, каждая вкладка прочитает этот файл.

Что помещать в каждый файл

ФайлУсловие выбораПодходящие вариантыЧего избегать
.zshenvКаждый Zsh с установленной RCSРедкие переменные, нужные каждому процессу ZshВывод, алиасы, промпты, сетевые вызовы, медленные команды
.zprofileLogin-shellНастройка path для login-сессии и экспортируемые значения по умолчаниюИнтерактивные хуки, completion, key bindings
.zshrcИнтерактивные shellПромпт, completion, алиасы, опции shell, хуки интерактивных инструментовВывод или изменения, предназначенные для скриптов
.zloginLogin-shell после .zshrcРедкие действия после интерактивного запускаКонфигурация, которая должна предшествовать .zshrc
.zlogoutЗавершение login-shellНебольшая очистка или сброс состояния терминалаВажное состояние, которое должно пережить сбой или exec

Пусть .zshenv остаётся скучным, если установлена RCS

Если установлена RCS, каждый неинтерактивный zsh -c читает .zshenv (руководство Zsh). echo, вызов package manager или дорогой subprocess в этом файле могут испортить вывод команд и замедлить скрипты. Большинству пользователей этот файл вообще не нужен либо в нём должно быть совсем немного настроек.

Экспортируемая переменная в .zprofile наследуется дочерними процессами этого login-shell. Она автоматически не передаётся несвязанным GUI-приложениям, сервисам или shell, родительский процесс которого этот файл не читал.

Используйте .zprofile для login-only настройки

Небольшой profile может задать экспортируемые значения по умолчанию и idempotent path:

# ~/.zprofile
export EDITOR=nvim
export VISUAL=nvim

# Zsh ties the path array to the PATH scalar.
typeset -U path PATH
path=("$HOME/.local/bin" $path)
export PATH

typeset -U удаляет дубликаты элементов массива. Это делает повторное подключение файла безопаснее, хотя в файлах запуска всё равно следует избегать лишней работы.

Используйте .zshrc для интерактивного состояния

# ~/.zshrc
setopt auto_cd hist_ignore_all_dups share_history

autoload -Uz compinit
compinit

alias ll='ls -lah'
bindkey -e

Алиасы, completion widgets, key maps, промпты и хуки смены директорий должны находиться в интерактивном shell, который ими пользуется.

Менеджеры версий выходят за границы простой схемы файлов

Совет «помещайте менеджеры версий в .zprofile, потому что они медленные» ненадёжен. Менеджер версий может генерировать несколько типов кода:

  • статическую переменную path или root
  • путь к command shim, необходимый дочерним процессам
  • определения completion
  • shell-функции, изменяющие текущий shell
  • хуки смены директорий для автоматического переключения версий

Интерактивные части должны присутствовать в каждом интерактивном shell, включая вложенный zsh, запущенный из редактора. Поэтому актуальная настройка fnm для Zsh помещает этот хук в .zshrc:

eval "$(fnm env --use-on-cd --shell zsh)"

Актуальная настройка pyenv для Zsh также помещает полную интерактивную инициализацию в .zshrc:

export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && path=("$PYENV_ROOT/bin" $path)
eval "$(pyenv init - zsh)"

Документация Pyenv описывает более узкий режим pyenv init --path для настройки shim-path, тогда как полный pyenv init также устанавливает completion и shell-функции. Перед разделением конфигурации изучите актуальную документацию инструмента и генерируемый им вывод.

Если запуск медленный, сначала измерьте его, а уже потом переносите код в режим, где он перестанет работать.

Не подключайте весь profile из .zshrc

Распространённый кроссплатформенный workaround выглядит так:

# Avoid this blanket coupling.
source ~/.zprofile

Так login-only действия превращаются в действия для каждого интерактивного shell, из-за чего могут повторно запускаться агент, доступ к keychain, вывод или изменение path.

Если и login-, и не-login интерактивным shell нужен небольшой набор статических экспортируемых переменных, вынесите его в idempotent-фрагмент и подключайте явно:

# ~/.config/zsh/environment.zsh
typeset -U path PATH
path=("$HOME/.local/bin" $path)
export PATH EDITOR=nvim VISUAL=nvim
# ~/.zprofile
source "$HOME/.config/zsh/environment.zsh"
# ~/.zshrc
if [[ ! -o login ]]; then
  source "$HOME/.config/zsh/environment.zsh"
fi

# Interactive-only configuration follows.

Этот паттерн отделяет общие данные от побочных эффектов login-запуска. Другой корректный вариант — единообразно настроить терминал или session manager, вместо того чтобы поддерживать оба режима.

Диагностируйте shell, который работает медленно или теряет состояние

Воспроизведите каждый режим напрямую:

zsh -lic 'print "login interactive"'
zsh -ic  'print "non-login interactive"'
zsh -lc  'print "login non-interactive"'
zsh -c   'print "non-login non-interactive"'

Измерьте именно тот режим, который фактически запускают пользователи:

time zsh -lic exit
time zsh -ic exit

Если источник проблемы неясен, включите trace выполнения файлов и строк:

PS4='%N:%i> ' zsh -xlic exit

Запускайте trace в чистом тестовом аккаунте или проверьте его перед публикацией: команды запуска могут раскрыть токены и другие чувствительные значения.

Если отсутствует исполняемый файл, проверьте и режим, и path:

print -r -- "interactive=$options[interactive] login=$options[login]"
print -l -- $path
whence -va python node uv

Заключение

Полезное различие — не «окружение против алиасов», а состояние, относящееся только к login-shell, и интерактивное состояние, плюс небольшой объём настроек, который должен влиять на каждый процесс Zsh, если установлена RCS.

Проверяйте флаги shell, не допускайте вывода при запуске и размещайте каждый сгенерированный хук там, где доступны все необходимые ему возможности. Так конфигурация будет переносимой между терминальными приложениями — без предположения, что в macOS и Linux существует только один фиксированный режим запуска.

Ссылки