🧙

【zsh】闇のpath_helperに対する防衛術 (※2025年時点)

2025/01/19に公開

この授業では path_helperchmod -x したり /etc/zprofilerm したりはしない。いいかな? zshの複雑な処理と芸術的な技を著者が扱えるとは期待しないでほしい。

だが一部の、シェルの設定に疲弊した者には伝授してやろう──

PATH の値を操り path_helper を惑わせる技を。変数を実行環境の中に詰め、OS間ポータビリティを維持し、global_rcs すら unsetopt する、そういうdotfilesを。

概要

path_helper はmacOSにデフォルトでインストールされているコマンドです。その名の通り、シェル環境における PATH の設定を補助・支援するものです。OS X 10.5 Leopard から導入され、2025年現在も macOS 14 Sonoma, macOS 15 Sequoia 双方に組み込まれています。

動作

このコマンドは /etc/paths および /etc/paths.d/* を読み込み、PATH の設定コードを出力します。

% /usr/libexec/path_helper -s/usr/libexec/path_helper -s
PATH="/usr/local/bin:.../Library/Apple/usr/bin"; export PATH;

これらのファイル・ディレクトリを一般ユーザが使用することはほとんどありません。
しかし、.NET framework for macやGolangなど、インストーラ経由で導入するアプリケーションの一部は現在でもこの仕組みを利用しています。

使用

/etc/profile, /etc/zprofile にコマンドの出力を eval するシェルスクリプトが記述されています。前者はbashが、後者はzshがログインシェルとして起動した際に読み込みます。

path_helper 問題

path_helper~/.zshenv より後に実行されます[1]。そのため、ここで PATH を設定した場合、想定と異なる順番に変わってしまう可能性があります。
たとえば:

# ~/.zshenv
export PATH=/opt/homebrew/bin:$PATH

と記述しても、ログイン後に確認すると

% echo $PATH | sed 's/:/\n/g'
/usr/local/bin           # <-- /etc/paths のエントリが先にくる
...
/Library/Apple/usr/bin
/opt/homebrew/bin        # <-- ~/.zshenv で追加したエントリはその後に並ぶ

このようになってしまいます。

この影響で、macOSビルトインのコマンドと同名のものをインストールした場合前者が優先されてしまう現象が発生します。pyenvrbenv などがうまく動作しなくなったり、自分でインストールしたはずなのにバージョンが変わらなかったりするのは path_helper が原因かもしれません。

歴史

path_helper の歴史は非常に長く語りきれないため、事前調査したスクラップへのリンクを添付しておきます。

https://zenn.dev/link/comments/0368135c2f9b30

要約すると、人類はこのシステムと15年以上戦い続けています。 Appleによるアップデートやエコシステムの更新、OSSの思想の変遷などさまざまな要因が複雑に絡み合った結果、 「path_helper問題」という単語の意味が数年ごとに移り変わる とんでもない状況が続いてきました。

解決策

いくつか有効な解決手法を列挙します。

1. ~/.zshenv~/.zprofile で変数を受け渡す

~/.zshenvPATH の設定を完了し、uname = Darwin かつログインシェルの場合のみ その内容を別名で export します。これを ~/.zprofile で受け取り、PATH に再設定することで、 path_helper の出力と ~/.zshenv の設定の双方を維持したままシェル環境を立ち上げることができます。

# ~/.zshenv
export PATH=...

if [[ -o login && "$(uname -s)" = 'Darwin' ]]; then
    export BACKUP_PATH=$PATH
    PATH=""
fi
# ~/.zprofile

if [[ "$(uname -s)" = 'Darwin' ]]; then
    typeset -U path PATH
    PATH=$BACKUP_PATH:$PATH
    unset BACKUP_PATH
fi

環境へ与える変更が少ない一方、煩雑でわかりづらいのがデメリットです。

2. ~/.zshenvno_global_rcs し、自ら path_helper を呼ぶ

バージョン3.1.6以上のzshには no_global_rcs というオプションが存在します[2]。これを設定すると /etc/zshenv 以外の /etc/z*一切読み込まれなくなります

昔はこれを使って path_helper 自体を無効化してしまう方法がよく使われていたようですが(詳細は歴史セクションを参照)、これは思わぬトラブルを招きます。現時点で利用実績がある以上、どんなに邪魔でも排除すべきではありません。

そこで あえて自らpath_helperを呼びます~/.zshenv の先頭で no_global_rcs を有効化しておき、 path_helper を実行して初期値を設定し、そこからゆっくり PATH を育てていきます。

# ~/.zshenv
setopt no_global_rcs
if [ -x /usr/libexec/path_helper ]; then
    eval `/usr/libexec/path_helper -s`
fi

export PATH=/opt/homebrew/bin:$PATH
...

変更が少なく構造も単純に済みますが、/etc/zshrc~/.zshrc から呼ぶ のを忘れないよう注意する必要があります。

# ~/.zshrc
if [ -r /etc/zshrc]; then
    . /etc/zshrc
fi

alias ll=ls -la
...

この手法の初出は不明ですが、KOBA789氏のdotfiles[3]で使われています。

3. ~/.zprofile から ~/.zshrc のどこかで PATH を設定する

macOSではほとんどのターミナルでログインシェルが起動する[4]ので、これでも解決します。
しかし、ショートカット.appやlaunchdデーモン経由の起動など、非インタラクティブ・非ログインかつログインまたはインタラクティブシェルの環境を継承せずに生成されたシェルでは PATH の設定されるタイミングがなくなってしまうため、注意が必要です。

シェル環境とその複製・継承についてはこちらをご参照ください。

https://zenn.dev/enchan1207/articles/fcc5762f49f5fc

まとめ

path_helper はOS Xの頃から存在し、開発者に地味な不便を強いてきた仕組みのひとつです。
正直 どこかのタイミングで廃止されるんじゃないか という気もしますが、現状まだまだ使われているようなので、我々Apple Developerはこれとうまく付き合っていかなければなりません。

私はこれまで ~/.zshenv~/.zshrc に分けて記述していましたが、この機に no_global_rcs を使った方法に移行しようと思います。dotfiles盆栽が捗る捗る……

参照文献

この記事は以下のスクラップをもとに作成しています。短くまとめる都合上大部分を省略しているため、歴史や動作などの詳細はこちらをご参照ください。

https://zenn.dev/enchan1207/scraps/f6a808e960787f

脚注
  1. スタートアップ/シャットダウン ファイル | Zsh - ArchWiki ↩︎

  2. New features in zsh version 3.1.6 (beta version) - Release Notes ↩︎

  3. dotfiles/.zsh/.zshenv at df18498c38b245be1286cf8e4f1e39960cdc16b4 · KOBA789/dotfiles ↩︎

  4. ターミナル.app では -il 、VSCodeでは -l が渡されるため。 ↩︎

Discussion