shellenv: emit zsh fpath in nested/child shells#21884
Conversation
The early-return idempotency check skipped all output when PATH already contained Homebrew's bin/sbin. Unlike PATH, zsh's fpath is a shell-local array not inherited by child processes, so completions broke in tmux/zellij panes and nested zsh shells. Move shell detection before the early return and always emit the fpath line for zsh. Fixes Homebrew#21879
There was a problem hiding this comment.
Pull request overview
Fixes brew shellenv for zsh in nested/child shells by ensuring the zsh fpath initialization is still emitted even when the PATH idempotency/early-return condition is met (so zsh completions work in tmux/zellij panes and nested zsh sessions).
Changes:
- Move shell detection ahead of the PATH-based early return in
shellenv.sh. - When early return triggers, still emit the
fpath[1,0]=...line for zsh. - Add an integration spec asserting
fpathoutput forbrew shellenv zsheven when PATH already contains the Homebrew prefix.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| Library/Homebrew/cmd/shellenv.sh | Reorders early-return logic and ensures zsh fpath is emitted in the idempotent PATH case. |
| Library/Homebrew/test/cmd/shellenv_spec.rb | Adds an integration test covering the zsh fpath output when PATH already includes Homebrew. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The unconditional fpath[1,0]= caused duplicate entries when
brew shellenv zsh was evaluated from both .zprofile and .zshrc
in the same login shell. Use zsh's ${fpath[(Ie)...]} exact-match
lookup so the directory is only prepended when not already present.
Deduplicate Homebrew's zsh site-functions entry while always moving it to the front of fpath. This keeps repeated `brew shellenv zsh` evals returning success and preserves Homebrew's completion precedence. Add a regression test for the pre-existing non-first fpath case.
CC @issyl0 can you take a look at this? |
MikeMcQuaid
left a comment
There was a problem hiding this comment.
Thanks @sumanthratna, some notes!
@MikeMcQuaid Hmm, I cannot reproduce these errors on this branch or |
|
@issyl0 no worries, thanks for trying! |
| # zsh's fpath is not inherited by child processes, so skip the early | ||
| # return for zsh to always emit the full output including fpath setup. |
There was a problem hiding this comment.
I think instead this should keep the early return if the zsh fpath is set correctly/as expected. Always bypassing early return just for zsh kinda sucks, particularly given it's the system default on macOS now. It'll slow it down for everyone always even if they don't care about their fpath being passed through to other shells. Sorry!
There was a problem hiding this comment.
correct me if I'm wrong -- I think checking fpath has the same challenge as MANPATH / INFOPATH above; not sure if we can check whether or not fpath is set correctly from within shellenv.sh
But if I've understood correctly, I think I've matched your other suggestions?
- No duplication in
fpathlogic - Don't bypass early return for all
zshusers - Idempotent — safe to re-eval, no fpath duplicates (since exported code includes
fpathcheck)
Let me know if any other feedback! Thanks!
| .and be_a_success | ||
| end | ||
|
|
||
| it "sets zsh fpath idempotently at the front across repeated evals", :integration_test do |
There was a problem hiding this comment.
Don't need a new integration test just for this but good to use it to verify yourself.
| # from here (not exported, filtered by bin/brew's env -i), so always emit it. | ||
| if [[ "${HOMEBREW_SHELL_NAME}" == "zsh" ]] || [[ "${HOMEBREW_SHELL_NAME}" == "-zsh" ]] | ||
| then | ||
| echo "if (( \${+fpath} )); then fpath=(\"${HOMEBREW_PREFIX}/share/zsh/site-functions\" \"\${(@)fpath:#${HOMEBREW_PREFIX}/share/zsh/site-functions}\"); else fpath=(\"${HOMEBREW_PREFIX}/share/zsh/site-functions\"); fi;" |
There was a problem hiding this comment.
| echo "if (( \${+fpath} )); then fpath=(\"${HOMEBREW_PREFIX}/share/zsh/site-functions\" \"\${(@)fpath:#${HOMEBREW_PREFIX}/share/zsh/site-functions}\"); else fpath=(\"${HOMEBREW_PREFIX}/share/zsh/site-functions\"); fi;" | |
| echo "fpath[1,0]=\"${HOMEBREW_PREFIX}/share/zsh/site-functions\";" |
is this insufficient?
| echo "export HOMEBREW_REPOSITORY=\"${HOMEBREW_REPOSITORY}\";" | ||
| if [[ "${HOMEBREW_SHELL_NAME}" == "zsh" ]] || [[ "${HOMEBREW_SHELL_NAME}" == "-zsh" ]] | ||
| then | ||
| echo "fpath[1,0]=\"${HOMEBREW_PREFIX}/share/zsh/site-functions\";" |
There was a problem hiding this comment.
Can we not just add an
echo "export FPATH;"here to make sure our fpath changes propagate to child shells?
There was a problem hiding this comment.
@carlocab If that works: yes, seems vastly preferable.
zsh's fpath is not an exported environment variable, so child shells (tmux/Zellij panes, nested zsh) lose Homebrew's completions path. The shellenv idempotency check only inspects PATH, which IS inherited, so it early-returns and never emits the fpath line. Exporting FPATH after setting fpath ensures child zsh shells inherit it. Fixes Homebrew#21879 Supersedes Homebrew#21884
The early-return idempotency check skipped all output when PATH already contained Homebrew's bin/sbin. Unlike PATH, zsh's fpath is a shell-local array not inherited by child processes, so completions broke in tmux/zellij panes and nested zsh shells.
Move shell detection before the early return and always emit the fpath line for zsh.
Fixes #21879
brew lgtm(style, typechecking and tests) with your changes locally?brew lgtmcomplains about unrelated typechecking configbrew shellenv zshwithPATHset to trigger the early return — confirmed it now emits thefpath[1,0]=line (previously produced zero output)brew shellenv bashwith the samePATH— confirmed it still produces no output on early return (no regression)brew style,brew typecheck, andbrew tests --only=cmd/shellenv— all pass