Файлы запуска Zsh: .zprofile и .zshrc в macOS и Linux
Автоматический перевод
Эта статья была автоматически переведена с оригинальной английской версии.
Если терминал работает медленно или переменная окружения есть в одном терминале, но отсутствует в другом, причина часто в режиме запуска Zsh. Чаще всего правят ~/.zprofile и ~/.zshrc, но по имени файла не понять, подходит ли ему конкретная строка. В статье показано, какие файлы запуска читает каждый вид Zsh, что помещать в каждый файл и какие команды покажут, в каком shell вы работаете.
Zsh выбирает файлы запуска по двум независимым признакам: является ли shell login-shell и является ли он интерактивным. Терминальные приложения в macOS и Linux могут выбирать разные сочетания, поэтому проверяйте запущенный shell, а не считайте, что всё решает операционная система.
Интерактивный shell предназначен для ввода команд, обычно из терминала; -i может принудительно включить этот режим. Login-shell запускается в режиме login, который выбирает запускающая программа или -l; это не означает, что shell запускается только при входе в компьютер (опции состояния shell в Zsh).
Zsh проверяет два независимых флага
Shell может быть:
- login и интерактивным, как новое окно Terminal в macOS или
zsh -l - не-login и интерактивным, как вложенный
zsh, запущенный из другого shell - login и неинтерактивным, как
zsh -lc 'command' - не-login и неинтерактивным, как
zsh -c 'command'
На диаграмме два флага расположены по двум осям. В каждой ячейке указано, какие программы обычно запускают такой shell, и отмечено, какие из пяти пользовательских файлов он читает.
Режим выбирает программа, которая запускает shell. Вот значения по умолчанию для распространённых способов запуска:
| Чем запущен | Режим по умолчанию |
|---|---|
| macOS Terminal, iTerm2 | login, интерактивный |
| GNOME Terminal | не-login, интерактивный |
| Терминал VS Code в macOS | login, интерактивный (zsh -l) |
| Терминал VS Code в Linux | не-login, интерактивный |
ssh host | login, интерактивный |
ssh host 'command' | не-login, неинтерактивный |
новое окно tmux при пустом default-command | login, интерактивный |
zsh, запущенный из shell | не-login, интерактивный |
zsh script.zsh | не-login, неинтерактивный |
Ваши настройки могут изменить любую строку. Реальный режим текущего shell покажет команда:
print -r -- "interactive=$options[interactive] login=$options[login]"
Фактический порядок запуска
При стандартных опциях запуска Zsh сначала читает глобальные файлы, затем соответствующий пользовательский файл. Порядок пользовательских файлов такой:
$ZDOTDIR/.zshenvпри каждом запуске Zsh$ZDOTDIR/.zprofileесли shell является login-shell$ZDOTDIR/.zshrcесли shell интерактивный$ZDOTDIR/.zloginесли shell является login-shell$ZDOTDIR/.zlogoutпри штатном завершении интерактивного login-shell
Если ZDOTDIR не задана, Zsh использует $HOME. Расположение общесистемных файлов зависит от установки; обычно это /etc/zshenv, /etc/zprofile, /etc/zshrc, /etc/zlogin и /etc/zlogout (руководство Zsh).
В macOS /etc/zprofile запускает path_helper, который ставит системные каталоги в начало PATH. Свои записи PATH задавайте в ~/.zprofile, который выполняется после него, а не в ~/.zshenv.
RCS — включённая по умолчанию опция Zsh, которая позволяет читать файлы запуска после /etc/zshenv. zsh -f отключает её и обычно пропускает остальные файлы запуска. /etc/zshenv всё равно выполняется и может снова включить RCS. Если проблема там исчезла, изучите пропущенную конфигурацию запуска (руководство Zsh).
Следующая диаграмма показывает полный порядок для нового окна macOS Terminal, включая глобальные файлы, и что происходит с записью в PATH, добавленной в ~/.zshenv, по сравнению с ~/.zprofile.
.zprofile выполняется один раз на каждый процесс login-shell, а не один раз за вход в компьютер. Если каждая новая вкладка терминала запускает login-shell, каждая вкладка читает этот файл.
Что помещать в каждый файл
| Файл | Правило выбора | Подходит | Избегайте |
|---|---|---|---|
.zshenv | Каждый Zsh | ZDOTDIR и редкие переменные, нужные каждому процессу Zsh | вывод, алиасы, приглашения, сетевые вызовы, медленные команды |
.zprofile | Login-shell | настройка путей login-сессии и экспортируемые значения по умолчанию | интерактивные хуки, автодополнение, привязки клавиш |
.zshrc | Интерактивные shell | приглашение, автодополнение, алиасы, опции shell, интерактивные хуки инструментов | вывод или изменения, предназначенные для скриптов |
.zlogin | Login-shell, после .zshrc | редкие login-действия, которые должны выполниться после .zshrc | конфигурация, которая должна идти до .zshrc |
.zlogout | Выход из login-shell | небольшая очистка или сброс терминала | важное состояние, которое должно пережить сбой или exec |
Держите .zshenv коротким и тихим
Каждый неинтерактивный zsh -c читает .zshenv (руководство Zsh). echo, вызов менеджера пакетов или тяжёлый подпроцесс в этом файле могут испортить вывод команд и замедлить скрипты. Большинству пользователей в нём нужно мало или ничего.
Обычное исключение — ZDOTDIR. Задайте её в ~/.zshenv, чтобы хранить остальные файлы запуска в другом каталоге, например ~/.config/zsh.
Используйте .zprofile для настройки только в login-shell
Экспортированную переменную из .zprofile наследуют дочерние процессы этого login-shell. В несвязанные GUI-приложения, службы и shell, чей родитель не читал файл, она автоматически не попадает.
Небольшой профиль в macOS может загружать Homebrew, задавать экспортируемые значения по умолчанию и собирать PATH без дублей:
# ~/.zprofile
# The Homebrew installer recommends this line on Apple silicon.
eval "$(/opt/homebrew/bin/brew shellenv zsh)"
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 оставляет каждую запись один раз. Без него каждый source файла или вложенный login-shell снова добавляет тот же каталог, и echo $PATH показывает его несколько раз.
Используйте .zshrc для интерактивного состояния
Новый процесс zsh наследует экспортированные переменные. Алиасы, функции, опции shell, привязки клавиш и хуки родителя он не наследует. Каждый интерактивный shell должен определить их заново, и это делает .zshrc. При обычных неинтерактивных запусках Zsh пропускает .zshrc.
Диаграмма сравнивает вложенный zsh и shell скрипта zsh -c, запущенные из одного login-shell. В этом примере показанное состояние shell задаёт только .zshrc.
Минимальный .zshrc задаёт опции, автодополнение, алиасы и привязки клавиш:
# ~/.zshrc
setopt auto_cd hist_ignore_all_dups share_history
autoload -Uz compinit
compinit
alias ll='ls -lah'
bindkey -e
Куда помещать хуки менеджеров версий
Совет «помещайте менеджеры версий в .zprofile, потому что они медленные» ненадёжен. Менеджер версий может выдавать код нескольких видов:
- статический путь или переменная корневого каталога
- путь к shim-командам, нужный дочерним процессам
- определения автодополнения
- функции shell, которые меняют текущий shell
- хуки смены каталога для автоматического переключения версий
Первые два экспортируются и передаются дочерним 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, тогда как полный pyenv init ещё и устанавливает автодополнение и функции shell. Прежде чем разделять инициализацию, изучите актуальную документацию инструмента и сгенерированный им код.
Если запуск медленный, сначала измерьте его, а не переносите код в режим, где он перестанет работать. Как это сделать, показывает раздел о диагностике.
Не подключайте весь .zprofile из .zshrc
Распространённый кроссплатформенный обходной путь:
# Avoid this blanket coupling.
source ~/.zprofile
Это превращает действия только для login-shell в действия для каждого интерактивного shell. Так можно повторно запустить ssh-agent, снова запросить доступ к связке ключей, вывести текст или дважды добавить записи в PATH.
Если и login-, и не-login интерактивным shell нужен небольшой набор статических экспортов, вынесите идемпотентный фрагмент и подключайте его осознанно:
# ~/.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-shell читает environment.zsh через .zprofile. Не-login интерактивный shell читает его через .zshrc. Login-действия вроде ssh-agent остаются в .zprofile, поэтому такой подход отделяет общие данные от побочных эффектов login. Другой вариант — настроить терминал на запуск login-shell, тогда каждая вкладка читает .zprofile.
Диагностика медленного shell или shell без нужного состояния
Начните с симптома: медленный 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"'
Для медленного shell используйте time для общего времени, zprof — чтобы узнать, какая функция занимает время, и трассировку -x — чтобы увидеть, какой файл и строка выполняются.
Измерьте режим, который запускает ваш терминал:
time zsh -lic exit
time zsh -ic exit
Загрузите профилировщик в начале .zshrc и выведите его отчёт в конце. Следующий новый shell выведет таблицу функций, отсортированную по времени:
# first line of ~/.zshrc
zmodload zsh/zprof
# last line of ~/.zshrc
zprof
Если источник неясен, трассируйте выполнение по файлам и строкам:
PS4='%N:%i> ' zsh -xlic exit
Запускайте трассировку в чистой тестовой учётной записи или просмотрите её перед публикацией: команды запуска могут раскрывать токены и другие секретные значения.
Если не находится исполняемый файл, проверьте и режим, и путь:
print -r -- "interactive=$options[interactive] login=$options[login]"
print -l -- $path
whence -va python node uv
Чтобы применить правку, откройте новую вкладку или выполните exec zsh -l (login) либо exec zsh (не-login). source ~/.zshrc повторно выполняет файл в текущем shell и сохраняет всё, что задала старая версия.
Заключение
Важное различие проходит не между «окружением и алиасами», а между состоянием только для login-shell и интерактивным состоянием, плюс небольшим набором настроек, которые должны действовать на каждый процесс Zsh.
Если shell ведёт себя неправильно, выполните команду print из первого раздела, а затем проверьте, что настройка лежит в файле, который читает этот режим.