Skip to content

shellenv: emit zsh fpath in nested/child shells#21884

Closed
sumanthratna wants to merge 12 commits into
Homebrew:mainfrom
sumanthratna:shellenv-fpath
Closed

shellenv: emit zsh fpath in nested/child shells#21884
sumanthratna wants to merge 12 commits into
Homebrew:mainfrom
sumanthratna:shellenv-fpath

Conversation

@sumanthratna

Copy link
Copy Markdown
Contributor

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


  • Have you followed the guidelines in our Contributing document?
  • Have you checked to ensure there aren't other open Pull Requests for the same change?
  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests (excluding integration tests) for your changes? Here's an example.
  • Have you successfully run brew lgtm (style, typechecking and tests) with your changes locally?
    • yes; brew lgtm complains about unrelated typechecking config
==> brew style --changed --fix
Library/Homebrew/test/cmd/shellenv_spec.rb:1:1: C: Sorbet/StrictSigil: Sorbet sigil should be at least strict got false.
# typed: false
^^^^^^^^^^^^^^
Library/Homebrew/test/cmd/shellenv_spec.rb:1:1: C: Sorbet/TrueSigil: Sorbet sigil should be at least true got false.
# typed: false
^^^^^^^^^^^^^^

  • AI was used to generate or assist with generating this PR. Please specify below how you used AI to help you, and what steps you have taken to manually verify the changes.
    • Claude Code (Opus 4.6 (1M context) with high effort) was used to read the issue, explore the relevant source code (shellenv.sh, shellenv_spec.rb, bin/brew), design the fix, and implement the changes.
    • The changes were manually verified by:
      1. Smoke-testing brew shellenv zsh with PATH set to trigger the early return — confirmed it now emits the fpath[1,0]= line (previously produced zero output)
      2. Smoke-testing brew shellenv bash with the same PATH — confirmed it still produces no output on early return (no regression)
      3. Running brew style, brew typecheck, and brew tests --only=cmd/shellenv — all pass

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
Copilot AI review requested due to automatic review settings April 1, 2026 18:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 fpath output for brew shellenv zsh even 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.
@MikeMcQuaid

Copy link
Copy Markdown
Member

==> brew style --changed --fix
Library/Homebrew/test/cmd/shellenv_spec.rb:1:1: C: Sorbet/StrictSigil: Sorbet sigil should be at least strict got false.

typed: false

^^^^^^^^^^^^^^
Library/Homebrew/test/cmd/shellenv_spec.rb:1:1: C: Sorbet/TrueSigil: Sorbet sigil should be at least true got false.

typed: false

^^^^^^^^^^^^^^

CC @issyl0 can you take a look at this?

@MikeMcQuaid MikeMcQuaid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @sumanthratna, some notes!

Comment thread Library/Homebrew/cmd/shellenv.sh
@issyl0

issyl0 commented Apr 2, 2026

Copy link
Copy Markdown
Member

==> brew style --changed --fix
Library/Homebrew/test/cmd/shellenv_spec.rb:1:1: C: Sorbet/StrictSigil: Sorbet sigil should be at least strict got false.
Library/Homebrew/test/cmd/shellenv_spec.rb:1:1: C: Sorbet/TrueSigil: Sorbet sigil should be at least true got false.

CC @issyl0 can you take a look at this?

@MikeMcQuaid Hmm, I cannot reproduce these errors on this branch or master? Test files are excluded from the Sorbet/TrueSigil and Sorbet/FalseSigil cops.

@MikeMcQuaid

Copy link
Copy Markdown
Member

@issyl0 no worries, thanks for trying!

Comment thread Library/Homebrew/cmd/shellenv.sh Outdated
Comment on lines +21 to +22
# 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 fpath logic
  • Don't bypass early return for all zsh users
  • Idempotent — safe to re-eval, no fpath duplicates (since exported code includes fpath check)

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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\";"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not just add an

echo "export FPATH;"

here to make sure our fpath changes propagate to child shells?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@carlocab If that works: yes, seems vastly preferable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @carlocab I tested this out and you're right; zsh does sync fpath and FPATH. I opened a separate clean PR: #21910

sumanthratna added a commit to sumanthratna/brew that referenced this pull request Apr 3, 2026
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
@sumanthratna
sumanthratna deleted the shellenv-fpath branch April 3, 2026 21:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

brew shellenv idempotency check skips fpath setup in child/nested zsh shells

5 participants