【zsh】闇のpath_helperに対する防衛術 (※2025年時点)
この授業では path_helper
を chmod -x
したり /etc/zprofile
を rm
したりはしない。いいかな? 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ビルトインのコマンドと同名のものをインストールした場合前者が優先されてしまう現象が発生します。pyenv
や rbenv
などがうまく動作しなくなったり、自分でインストールしたはずなのにバージョンが変わらなかったりするのは path_helper
が原因かもしれません。
歴史
path_helper
の歴史は非常に長く語りきれないため、事前調査したスクラップへのリンクを添付しておきます。
要約すると、人類はこのシステムと15年以上戦い続けています。 Appleによるアップデートやエコシステムの更新、OSSの思想の変遷などさまざまな要因が複雑に絡み合った結果、 「path_helper問題」という単語の意味が数年ごとに移り変わる とんでもない状況が続いてきました。
解決策
いくつか有効な解決手法を列挙します。
~/.zshenv
と ~/.zprofile
で変数を受け渡す
1. ~/.zshenv
で PATH
の設定を完了し、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
環境へ与える変更が少ない一方、煩雑でわかりづらいのがデメリットです。
~/.zshenv
で no_global_rcs
し、自ら path_helper
を呼ぶ
2. バージョン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]で使われています。
~/.zprofile
から ~/.zshrc
のどこかで PATH
を設定する
3. macOSではほとんどのターミナルでログインシェルが起動する[4]ので、これでも解決します。
しかし、ショートカット.appやlaunchdデーモン経由の起動など、非インタラクティブ・非ログインかつログインまたはインタラクティブシェルの環境を継承せずに生成されたシェルでは PATH
の設定されるタイミングがなくなってしまうため、注意が必要です。
シェル環境とその複製・継承についてはこちらをご参照ください。
まとめ
path_helper
はOS Xの頃から存在し、開発者に地味な不便を強いてきた仕組みのひとつです。
正直 どこかのタイミングで廃止されるんじゃないか という気もしますが、現状まだまだ使われているようなので、我々Apple Developerはこれとうまく付き合っていかなければなりません。
私はこれまで ~/.zshenv
と ~/.zshrc
に分けて記述していましたが、この機に no_global_rcs
を使った方法に移行しようと思います。dotfiles盆栽が捗る捗る……
参照文献
この記事は以下のスクラップをもとに作成しています。短くまとめる都合上大部分を省略しているため、歴史や動作などの詳細はこちらをご参照ください。
Discussion