Файлы запуска 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, и отмечено, какие из пяти пользовательских файлов он читает.

Сетка два на два из флагов login и interactive. В каждой ячейке указано, кто запускает такой shell, и отмечено, какие из .zshenv, .zprofile, .zshrc, .zlogin и .zlogout он читает; интерактивная ячейка без login пропускает .zprofileСетка два на два из флагов login и interactive. В каждой ячейке указано, кто запускает такой shell, и отмечено, какие из .zshenv, .zprofile, .zshrc, .zlogin и .zlogout он читает; интерактивная ячейка без login пропускает .zprofile

Режим выбирает программа, которая запускает shell. Вот значения по умолчанию для распространённых способов запуска:

Чем запущенРежим по умолчанию
macOS Terminal, iTerm2login, интерактивный
GNOME Terminalне-login, интерактивный
Терминал VS Code в macOSlogin, интерактивный (zsh -l)
Терминал VS Code в Linuxне-login, интерактивный
ssh hostlogin, интерактивный
ssh host 'command'не-login, неинтерактивный
новое окно tmux при пустом default-commandlogin, интерактивный
zsh, запущенный из shellне-login, интерактивный
zsh script.zshне-login, неинтерактивный

Ваши настройки могут изменить любую строку. Реальный режим текущего shell покажет команда:

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

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

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

  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).

В macOS /etc/zprofile запускает path_helper, который ставит системные каталоги в начало PATH. Свои записи PATH задавайте в ~/.zprofile, который выполняется после него, а не в ~/.zshenv.

RCS — включённая по умолчанию опция Zsh, которая позволяет читать файлы запуска после /etc/zshenv. zsh -f отключает её и обычно пропускает остальные файлы запуска. /etc/zshenv всё равно выполняется и может снова включить RCS. Если проблема там исчезла, изучите пропущенную конфигурацию запуска (руководство Zsh).

Следующая диаграмма показывает полный порядок для нового окна macOS Terminal, включая глобальные файлы, и что происходит с записью в PATH, добавленной в ~/.zshenv, по сравнению с ~/.zprofile.

Порядок запуска login-shell в macOS от /etc/zshenv до /etc/zlogout с примерами PATH: ~/.local/bin, добавленный в ~/.zshenv, оказывается последним после path_helper, а добавленный в ~/.zprofile остаётся первымПорядок запуска login-shell в macOS от /etc/zshenv до /etc/zlogout с примерами PATH: ~/.local/bin, добавленный в ~/.zshenv, оказывается последним после path_helper, а добавленный в ~/.zprofile остаётся первым

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

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

ФайлПравило выбораПодходитИзбегайте
.zshenvКаждый ZshZDOTDIR и редкие переменные, нужные каждому процессу Zshвывод, алиасы, приглашения, сетевые вызовы, медленные команды
.zprofileLogin-shellнастройка путей login-сессии и экспортируемые значения по умолчаниюинтерактивные хуки, автодополнение, привязки клавиш
.zshrcИнтерактивные shellприглашение, автодополнение, алиасы, опции shell, интерактивные хуки инструментоввывод или изменения, предназначенные для скриптов
.zloginLogin-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.

Login-shell и два новых процесса zsh: оба получают копии PATH и EDITOR; вложенный zsh заново задаёт показанное состояние из .zshrc; shell zsh -c пропускает эту настройкуLogin-shell и два новых процесса zsh: оба получают копии PATH и EDITOR; вложенный zsh заново задаёт показанное состояние из .zshrc; shell zsh -c пропускает эту настройку

Минимальный .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 из первого раздела, а затем проверьте, что настройка лежит в файле, который читает этот режим.

Источники