Closed29

macのPATH設定について

ピン留めされたアイテム
enchanenchan

About this scraps

macOSの path_helper について掘り下げて悩んでまとめる

enchanenchan

path_helper とは

はじめにこのコマンドの概要を整理する。

enchanenchan

macOSの /usr/libexec/path_helper にデフォルトで存在するコマンド。man曰く、

helper for constructing PATH environment variable

The path_helper utility reads the contents of the files in the directories /etc/paths.d and /etc/manpaths.d and appends their contents to the PATH and MANPATH environment variables respectively.

つまり特定のディレクトリ以下にあるファイル群からPATHを構成・出力するもの。

enchanenchan

このコマンド自体は /etc/zprofile, /etc/profileで呼ばれている:

if [ -x /usr/libexec/path_helper ]; then
    eval `/usr/libexec/path_helper -s`
fi

zprofile, profile はシェルのスタートアップファイルのひとつ。
前者はzsh、後者はbash等がログインシェルとして起動したときに読み込まれる[1]

Catalina以降のmacOSではzshがデフォルトなので[2]、以降はzshをメインに取り扱う

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

  2. zsh を Mac のデフォルトシェルとして使う - Apple サポート (日本) ↩︎

enchanenchan

シェル起動時に -l を与えるとログインシェルとして起動する。
現在のシェルプロセスがログインシェルかどうかは以下のスクリプトで確認できる[1]:

if [[ -o login ]]; then echo "login"; else echo "not login"; fi # 'login' or 'not login'

ターミナル.appでは、「デフォルトのログインシェル」が選択されている場合は毎回 -il が渡される = ログインシェルかつインタラクティブシェルとして起動する

% ps aux | grep /bin/zsh
user      26312   0.0  0.0 410808784   4560 s008  Ss+   3:38PM   0:00.34 /bin/zsh -il

VSCodeはデフォルトで -l が渡される (terminal.integrated.profiles.<platform> で設定できる[2])

脚注
  1. Chapter 2: What to put in your startup files | A User's Guide to the Z-Shell ↩︎

  2. Terminal Profiles in Visual Studio Code ↩︎

enchanenchan

明示的にオプションが渡されなければログインシェルにはならない。

  • パスを指定して実行した場合 (./script.sh, /path/to/script.sh)、スクリプトが実行されるのは非ログインシェルとなる。shebangで -l を設定すると、再度ログイン処理(?)が走る。
  • built-in command[1]. で実行した場合(. ./script.sh)は、現在の環境を引き継ぐため、スクリプトが実行されるのはログインシェルとなる。
  • zsh に直接渡した場合は (当然ながら) コマンドラインオプションに依存する。
脚注
  1. Shell Command Language ↩︎

enchanenchan

path_helper 問題とは

ここでは 2025年1月時点での path_helper が引き起こしている問題の概要を整理する。

enchanenchan

要約すると シェル起動時にPATHの順序が変わってしまう 問題。
pythonruby, nano など、macOSにデフォルトで入っているプログラムを自分でインストールした場合、macOSのデフォルトが優先されてしまうことがある。

影響を受けるのがPATHというuser-dependentな値であること、また歴史的経緯が非常に複雑なことから、2025年現在でも完全無欠の解決策は示されていない(調査した限り)。

enchanenchan

根本的な原因は path_helper の実行タイミングにある。

先述の通り、これを呼ぶコードは /etc/zprofile に記述されている[1]。このファイルは、zshがログインシェルとして起動した際に /etc/zshenv, ~/.zshenv の後、~/.zprofile の前に読み込まれる。

そのため、仮にユーザが ~/.zshenvPATH を設定していた場合 シェル起動からログイン完了までの間に値が書き換えられてしまう。 これが path_helper 問題を引き起こしている。

脚注
  1. OS X El Capitan以降, 後述 ↩︎

enchanenchan

なお最近[1]のmacOSでは PATHの早い段階に /usr/local/bin がくるようになったため、この問題が発生しない環境も存在する。具体的には

  • Yosemite以降のmacOSを搭載したIntel mac
  • Homebrewが /usr/local/bin にインストールされており、バイナリのインストール先が /usr/local/Cellar で、シンボリックリンクが /usr/local/bin に張られている
  • ~/.zshenv, ~/.zprofile. ~/.zshrcPATH を操作するコードがない
  • ターミナル.appまたはVSCode Integrated terminalを使っているか、シェルの起動引数に -l が渡されている
  • インタラクティブシェルで直接コマンドを実行する

以上の条件を満たす場合、Homebrewでインストールしたパッケージについて path_helper で苦労することはほぼない。

例外としては、pyenvやrbenvなど macOSビルトインと同名のコマンドを /usr/local/bin 以外に配置して使う ことを想定しているソフトウェアが挙げられる。

脚注
  1. OS X Yosemite以降, 後述 ↩︎

enchanenchan

歴史

OS X Leopard で追加されて以来、 path_helper はさまざまな問題を引き起こしてきた。
ここではその歴史を振り返る。

enchanenchan

2007/10/26: OS X 10.5 Leopard リリース, path_helper 誕生

最初期バージョンの path_helper がリリースされた[1][2]

当時はシェルスクリプトとして実装されており、 /etc/zshenv から呼ばれていた[3]。また「/etc/paths および /etc/paths.d を読んでPATHを生成する」一連の動作については、当初から疑問視する声が上がっていた[4]

また、この時点では path_helper 実行前の PATH を引き継いでしまう現象 が発生していた[5](Snow Leopardで修正された)。

脚注
  1. 最近のMac OSXで、PATHをスマート(?)に管理するやり方。 - こせきの技術日記 ↩︎

  2. PATH and other evironment issues in Leopard ↩︎

  3. Snow Leopard時代のパス管理術 - builder by ZDNet Japan ↩︎

  4. kilala.nl - path_helper: sometimes Apple does kludgy, stupid things ↩︎

  5. Mastering the path_helper utility of MacOSX ↩︎

enchanenchan

2009/08/28: OS X 10.6 Snow Leopard リリース

path_helper の実装がシェルスクリプトからバイナリに変更された。また、当時は opensource.apple.com にてソースコードが公開されていた(2021年に削除, 現在でもアーカイブ[1]は閲覧可能)。

脚注
  1. Source Browser - opensource.apple.com ↩︎

enchanenchan

2013/01/27: path_helper の呼出し箇所に関する議論

zshのフレームワークpreztoのリポジトリにて「path_helper を無効化してはいけない」という旨のissueが起票された[1]。ここでは様々な論点・立場から激しい議論が繰り広げられた。

  • インストーラ等により使われる可能性があるから、 path_helper を無効化してはいけない
  • path_helper/etc/zshenv ではなく /etc/zprofile で呼ばれるべきだ
  • path_helperPATH を並べ変える "壊れた" コマンドであるから、無効化すべきだ
  • そもそもzshが /etc/zshenv 以外のグローバルrcファイルを読まないようにすべきだ
  • ユーザが /etc/paths を直接書き換えるべきだ

いずれも「解決できること」と「解決できないこと」または「懸念として残ること」があった。
解決策は見つからぬまま、このissueは2日後にリポジトリのオーナーによってクローズされた。


冷静に考えると、 /etc/zshenvpath_helper が実行されているなら、 ~/.zshenv で設定した PATH は正しく設定されるような気もする(~/.zshenv の方が後に読み込まれるため)。

しかし path_helper はそれまでの PATH の値を引き継がないので(Leopardでのバグ修正によるもの)、非インタラクティブまたは非ログインシェルでは ~/.zshrc~/.zprofile で設定した PATH が反映されないという懸念がある。

脚注
  1. 2013/01/27, The notice about path_helper on OS X is incorrect · Issue #381 · sorin-ionescu/prezto ↩︎

enchanenchan

2014/10/16: OS X 10.10 Yosemite リリース

このタイミングで /etc/paths の内容が変更され、 /usr/local/bin が先頭にくるようになった[1][2][3]。この影響かHomebrewの動作が不安定になり、issueが起票された[4]

これがどのような意図で行われたのかは不明で、2025年現在も /etc/paths のエントリ順は変わっていない。


この変更により、多くの環境で path_helper は問題視されなくなった。理由は以下のとおり。

  • Macを使う開発者の多くはHomebrewを使用している
  • Homebrewは /usr/local/Cellar にコマンドの実体、/usr/local/bin にそのシンボリックリンクを張る
  • /etc/paths という最上位のファイルで /usr/local/bin が優先されるようになったため、相当変なことをしない限りHomebrew版のコマンドを問題なく呼び出せる
脚注
  1. bash - OS X 10.10.4 Yosemite, has the default PATH changed? - Stack Overflow ↩︎

  2. macos - What is the Default content in /private/etc/paths? - Ask Different ↩︎

  3. Mac OS X YosemiteにPHPの開発環境を構築してみる(1) Xcode、Homebrew編 - ykiraの開発ブログ ↩︎

  4. (10.10) not recognizing /usr/local/bin before /usr/bin · Issue #29843 · Homebrew/legacy-homebrew ↩︎

enchanenchan

2015/09/30: OS X El Capitan リリース

このバージョンから、path_helper の呼び出しコードが /etc/zprofile に移動した[1][2][3]
これは前述のissueで議論が紛糾したため……もあるかもしれないが、 他シェルとの挙動を揃える意図 があったのではないだろうか?

例えばbashでは path_helper の呼び出しコードは /etc/profile に記述されているため、ログインシェルとして起動した場合にのみ実行される。これと同様の動作をzshで実現するために、 /etc/zprofile へ呼び出しコードが移動された……とも推測できる。


なお、この変更により path_helper~/.zshenv より後で実行されるようになったため、我々は ログインシェルにおいて ~/.zshenv で素直に PATH を設定する手段を失ってしまったことになる。
やるとすれば別の変数で受けて ~/.zshrc で反映させるか、非ログインシェルとして起動する (ターミナルの起動引数に -l を渡さない) しかない。

脚注
  1. Mac OSX El Capitan での ~/.zshenv の扱いについて #MacOSX - Qiita ↩︎

  2. El Capitanにしたらzsh上でのPATHが上書きされた - すぎゃーんメモ ↩︎

  3. Re: PSA: Mac OS X El Capitan upgrade might break your $PATH ↩︎

enchanenchan

2018/11/24: zshのインストールオプション --disable-etcdir の削除

Homebrewの方針変更に伴い、zshのインストールオプション --disable-etcdir が削除された[1]

このオプションは、zshが /etc/zshenv 以外の /etc/z* を読まないようにするためのものである。これを用いた解決策は2012年ごろからインターネット上で共有されていたが、この変更に伴い使用できなくなった。
なお2025年1月16日現在、brewのzshには --enable-etcdir=/etc が渡されており[2]/etc/z* を読むものとしてビルドされている。

脚注
  1. zsh: default unicode9, remove options. by MikeMcQuaid · Pull Request #34422 · Homebrew/homebrew-core ↩︎

  2. Homebrew/homebrew-core: Formula/z/zsh.rb ↩︎

enchanenchan

2020/11/17: Apple Siliconを搭載したMacの発売開始

いわゆる M1 Mac の最初のモデルが発売された。HomebrewはIntel-Macでは /usr/local/bin、そうでなければ /opt/homebrew/bin にインストールされるようになった。
これに伴い後者にはデフォルトでPATHが通らなくなったため、brew導入時にこのようなメッセージが出力されるようになった:

==> Next steps:
- Run these two commands in your terminal to add Homebrew to your PATH:
(echo; echo 'eval "$(/opt/homebrew/bin/brew shellenv)"') >> /Users/username/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"

曰く brew shellenv の出力を ~/.zprofile にペーストせよとのこと。brew shellenv とは:

Print export statements. When run in a shell, this installation of Homebrew will be added to your PATH, MANPATH, and INFOPATH.

export 文を出力します。シェルで実行した場合、Homebrewのインストールが PATH、MANPATH、および INFOPATH に追加されます。

つまりbrewのために PATH をいい感じに設定するスクリプトを生成・出力してくれるコマンドである。

enchanenchan

解決策とされる手法の考察

上述のとおり、path_helper はその実装以来多くの問題を引き起こしてきた。課題として浮上するたびに様々な解決策(とされる手法)が提起・検討されてきた。

これまで提案されてきたそれら手法について、個人的見解を主にまとめる。

enchanenchan

解決策1. /etc/zshenv/etc/zprofilemv する

要約: OS X El Capitan 以降では効果がない。

El Capitan以前のmacOSでは /etc/zshenv が存在し、そこで path_helper が呼ばれていた。そのためサブシェルを立ち上げるたびに PATH がリセットされてしまい、問題となった。
現在では(クリーンインストールであれば) /etc/zshenv は存在せず、path_helper の呼び出しコードは /etc/zprofile に移動した ため、効果はない。

enchanenchan

解決策2. path_helper をなんらかの方法で無効化あるいはバイパスする

要約: 予期せぬ不具合を生じ得る。

chmod で実行権限を剥奪したり、 no_global_rcs を有効化したり、 --disable-etcdirconfigure に入れてビルドしたりして path_helper を無効化することができる。
しかし一部のソフトウェア(特にインストーラで入れるタイプのもの)は path_helper の動作を前提とするものがある ため、健康的な解決策とは言い難い。

具体的には /etc/paths.d/ にエントリを追加し、自身へ PATH を通す処理を path_helper に任せてしまうというもの。2025年現在でも.NET Framework for macやGolangなどがこの方式を採用している。
PATH くらい自分で通すからいいぜ!という主張もあるかもしれないが、path_helper に悩むすべてのユーザにそれを強いるのは、果たして美しい解決策だろうか?

enchanenchan

解決策3. /etc/paths あるいは /etc/paths.d を書き換える

要約: 予期せぬ不具合を生じ得る。

~ より上のファイルを単一ユーザのために書き換える行為はマルチユーザ環境で予期せぬ不具合を生じる可能性が高いため、すべてのユーザに推奨できる方法とは言い難い。

まさか /etc/paths/Users/username/bin などと書くわけにはいかない。

enchanenchan

解決策4. path_helper の代替実装をインストールする

要約: 外部プログラムに根幹を任せてよいのか?

GitHub等で検索すると、path_helperの代替を謳うリポジトリがいくつか見つかる。これらの一部には今でも正しく動作するものもあるかもしれないが……

代替実装のソースを全て読み、動作とその安全性を確認し、運用する…… 果たして、PATH のためにそこまでする必要はあるだろうか?

enchanenchan

解決策5. PATH~/.zshrc で設定する

要約: スタートアップファイルの目的にそぐわないのでは?

.zshrc はzshがインタラクティブシェルとして起動した際 path_helper より後に読み込まれるため、ほとんどの場合はこれで解決する

しかしzshのintroductionによれば .zshenv (中略) should contain commands to set the command search path, plus other important environment variables. とあることから[1]、本来 PATH の設定コードは ~/.zshenv におくのが望ましい といえる。

また、非インタラクティブシェルでは読み込まれないこと、対話型シェルをサブシェルとして起動するたびに再設定されることなどを踏まえると、.zshrcに置くのはその目的からやや外れているといえる。

脚注
  1. Startup Files - An Introduction to the Z Shell ↩︎

enchanenchan

結論

調査結果をもとに2025年1月現在最適な構成を考える。

  • OS: macOS Sonoma あるいは Sequoia
  • シェル: zsh
  • 目的:
    • path_helper 問題の解決
    • OS間ポータビリティを維持したスタートアップファイルの構成
enchanenchan

(改めて) 問題整理

path_helper はmacOSにのみ存在するコマンドである。/etc/zprofile で呼び出され、~/.zshenv で設定した PATH の順番を書き換えてしまう。そのため、 python, ruby, sed などmacOSビルトインのものと同名のコマンドを自らインストールした際、パスによってはシステムデフォルトのものが優先されてしまう場合がある。

path_helper は無効化すべきでないため、我々はどうにか /etc/zprofile より後に PATH を編集 しなければならない。ここで "編集" とは、ログインシェルあるいは派生した環境[1] でのみ 、システムデフォルトより自分でインストールしたコマンドが優先されるようにPATHの順序を入れ替える操作を指す。

脚注
  1. 環境とは? 環境変数とは? ↩︎

enchanenchan

簡単な解決策

最も容易な解決策は ~/.zprofile ~ ~/.zlogin までのいずれかのファイルで PATH を設定する 方法である。一般的なシチュエーションではこれで解決する。

なぜ解決するのか

先述したファイル群はそれぞれ読み込まれる条件が異なるのに、なぜそんなテキトーな方法で解決するのか?

~/.zprofile ~/.zshrc ~/.zlogin
ログインシェル
インタラクティブシェル

macOSの一般的なターミナルエミュレータ (ターミナル.appやVSCode統合ターミナルなど) では、起動時の引数に -l が渡される。つまり毎回ログインシェルとして起動するため、何も考えずにGUIからターミナルを開くぶんには、これらファイルはどこかのタイミングで全て読み込まれる

「おい、シェルスクリプトから実行するときに困るじゃないか」?
困らない。なぜなら シェルスクリプトを叩くときは基本的になんらかのログインあるいはインタラクティブシェルを経由するはず だからだ。
zshでは PATH にはエクスポート属性が設定されている。したがって、シェルスクリプトの実行環境を継承した親環境のうちのどこかで PATH が設定されていれば、問題なく動作する。

ゆえにこのラフな手法でなんとかなる……というより、なんとかなってしまう

enchanenchan

落とし穴

さて、このラフな手法には 意外な落とし穴 がある。

1. ssh 経由で直接コマンドを実行

macOSでは、「システム環境設定」>「一般」>「共有」>「リモートログイン」を有効化することでsshサーバが起動する。

では、 ssh に直接コマンドを渡してみよう。

% ssh 127.0.0.1 printenv
(省略)
(user@127.0.0.1) Password:
LANG=ja_JP.UTF-8
(省略)
PATH=/usr/bin:/bin:/usr/sbin:/sbin
(省略)

path_helper も動作しないし、 ~/.zshrc で設定したパスも設定されない。なぜならこのシェルは非ログインかつ非インタラクティブなシェルだからだ。この場合、zshが読み込むのは /etc/zshenv~/.zshenv のみとなる。

2. ショートカット.app から実行

ショートカット.app には シェルスクリプトを実行 というブロックがある。

当然ながら、このシェルは非ログインかつ非インタラクティブなシェルである:

なら printenv も同じ値になるのか……と思いきやそんなことはなく、なんと path_helper 準拠のパスが設定される。なのに ~/.zprofile は呼ばれない 。また ~/.zshenvは呼ばれるが、 path_helper が出力するパスはそれより後に来る

……ユーザが望んでいたのは、この環境だったんじゃないか……?


脱線したが、まとめると

  • ~/.zprofile 以降のファイルで PATH を再設定するのは、簡単な解決策
    • 現在のmacOSにおけるほとんどのユースケースではこれで解決してしまう
  • いくつか落とし穴がある
    • ssh経由でインタラクティブシェルを介さずコマンドを実行した場合
    • ショートカット.app から実行した場合
enchanenchan

解決案

健康的に回避・解決する手段はいくつか考えられる。

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

~/.zshenvPATH の設定を完了し、「Darwinかつログインシェルなら、環境変数 PATH_BACKUP に現時点の PATH を保存する」処理を入れる。~/.zprofilePATH_BACKUP を受け取り、PATH に反映する。
これで、path_helper の出力を尊重しつつ PATH をいい感じにすることができる。

  • メリット: 環境の変更が少ない。
  • デメリット: 煩雑。

2. ~/.zshenvno_global_rcs してから path_helper を呼ぶ

under-controlでない部分をスキップしつつ、意図的に path_helper を呼び出し、その後でゆっくりPATHを編集するというもの。GitHubではKOBA789氏のdotfilesに採用されている[1]が、初出は不明。もしかすると氏が初出かもしれない。

  • メリット: 構造が単純。
  • デメリット: no_global_rcs しているので、 ~/.zshrc/etc/zshrcsource する必要がある。

3. ~/.zpath 的なファイルを作り source する

PATH の設定を別ファイルに退避させ、 ~/.zshenv~/.zprofile で読み込むというもの。

  • メリット: PATH を外部に置けるので、必要箇所で読み込めばなんとかなる。
  • デメリット: 非標準のファイルが増える。
脚注
  1. dotfiles/.zsh/.zshenv at df18498c38b245be1286cf8e4f1e39960cdc16b4 · KOBA789/dotfiles ↩︎

このスクラップは2025/01/19にクローズされました