enchanのメモ書き

計算機とフリルとラブライブ!

$PATHが勝手に書き変わる!?

…事件は、profileのお掃除中に起きた。

(どうやら、この案件は path_helper問題として知られているようです。この記事はそれに辿り着くまでにEnchanが遠回りをするお話。)


みなさんは普段、なんのシェルを使っていますか?

bash, zsh, fish, ....

私はずっとbashを使っていたのですが、少し前にzshに乗り換えた民だったりします。 若干クセがあるように感じますが、正直これはこれでありかなと思っています。
別方向の議論が始まってしまいそうなのでこの辺でやめておきましょう。

シェルのprofile

ひとくちにシェルといってもさまざまなものがありますが、ほとんど(全て?)のシェルに共通して 設定ファイル という概念があります。一般常識です。
bashなら .bash_profile.bashrc、zshなら.zshenv, .zshrc, .zprofile 等々、読み込まれるタイミングや順番などによってさまざまな名前がついており、 コマンドエイリアスや環境変数の設定の際はここに値を書くことが多いかと思います。

今回注目していくのは PATH です。パスを通す なんてよく言われる例のアレ。
私は .bash_profile, .zshenvなどに書くことが多いです。(どうやら.zshenvに書くべきではないようですが…)

どこでコピペしても基本的に動くように、基本構成はこんな感じにしています:

#
# zshenv
#

# Initial PATH
export PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:/Library/Apple/usr/bin

... (略) ...

# Python
export PATH=/usr/local/opt/python@3.9/bin/:$PATH

... (略) ...

# Custom built
export PATH=$HOME/local/bin:$PATH

# load .zshrc if exists
test -r ~/.zshrc && . ~/.zshrc

# prompt
export PS1="%n@%m %1~ %# "

# shell history path
export HISTFILE=${HOME}/.zsh_history

パスがなんらかの要因で吹っ飛んでも困らないように 一応の初期値と、そのほか追加で入れたアプリケーションへのパスを先に通しています。
下の方で.zshrcを読み、プロンプトを設定し、シェル履歴の設定を一通り終わらせたうえで .zshenvの処理が終了するとそういうわけです。

つまり、ログイン後の$PATHは以下のようになっているはずです:

/Users/user_name/local/bin

... (略) ...

/usr/local/opt/python@3.9/bin/

... (略) ...

/usr/local/bin
/usr/local/sbin
/usr/bin
/usr/sbin
/bin
/sbin
/Library/Apple/usr/bin

定義した順にパスが並び、一番最後にPATHの初期値 (# Initial PATH 以降で設定した値) が並んでいるはずです。
というか実際に .zshenv 内で echo $PATH | sed -e "s/:/\n/ig" を実行するだけで同じ結果が得られます。

ところが。

パスの並びが変わっている

ログイン直後に同様のコマンドを実行すると…

% echo $PATH | sed -e "s/:/\n/ig

/usr/local/bin
/usr/bin
/bin
/usr/sbin
/sbin
/Library/TeX/texbin
/Library/Apple/usr/bin
/Users/user_name/local/bin

... (略) ...

/usr/local/opt/python@3.9/bin/

... (略) ...

/usr/local/sbin

なんだか大変な順番になってしまっています。

PATHの初期値…のようなものが先頭に来てしまっており、 これでは ~/ 以下にインストールしたコマンドをすぐに呼び出せなくなってしまいます。
(~/local/bin/usr/local/binに同名のコマンドがあった場合、後者が優先されてしまう)

設定ファイルの適用順を整理する

もしかすると私の知らない設定ファイルがあるかもしれないので、一旦zshの設定ファイルを見にいきます。
ArchWikiの「Zsh」の項目によれば…

wiki.archlinux.jp

ログイン時、Zsh は以下のファイルをこの順番で読み込むようです:

  • /etc/zsh/zshenv
  • $ZDOTDIR/.zshenv <--- ここでPATHを初期化
  • /etc/zsh/zprofile
  • /etc/profile
  • $ZDOTDIR/.zprofile
  • /etc/zsh/zshrc
  • $ZDOTDIR/.zshrc
  • /etc/zsh/zlogin
  • $ZDOTDIR/.zlogin <--- コマンドを実行できる状態になるのはこれ以降
  • /etc/zsh/zlogout
  • $ZDOTDIR/.zlogout

PATHを設定しているのは ~/.zshenv なので、そこから$ZDOTDIR/.zlogin までのファイルで変更が加わる可能性があります。
(なお、手元の環境ではちょっとパスが違っていた(man参照)ので適宜読み替えています)

まず/etc/zsh/zprofile。macでは /etc/zprofile にありました。 /usr/libexec/path_helper に引数を渡して呼び出しているようです。

ん?

path_helperって…なんでしたっけ?

macOSのおせっかい(超絶失礼)

manによれば、path_helperとは PATH環境変数を構築するためのヘルパー であり…

path_helperユーティリティは、ディレクトリ /etc/paths.d および /etc/manpaths.d 内のファイルの内容を読み取り、 それらの内容をそれぞれ PATH および MANPATH 環境変数に追加します。(MANPATH 環境変数は、環境にすでに設定されていない限り変更されません。)
これらのディレクトリ内のファイルには、1行に1つのパス要素が含まれている必要があります。 これらのディレクトリを読み取る前に、デフォルトのPATH値とMANPATH値がそれぞれファイル /etc/paths と /etc/manpaths から取得されます。

…とのこと。つまりOSの都合の良いようにPATHを編集する コマンドのようです(超絶失礼)。
つまり.zshenv等をいじることで解決できる問題ではないということになります。 かなりイヤです。

対策

先述の通り、この案件は path_helper問題として知られており、さまざまな方々によって回避や対策が試みられているようです。

kiririmode.hatenablog.jp

qiita.com

this.aereal.org

/etc/paths に目的のパスを追加してもよかったのですが、管理者権限ないといじれないファイルに設定散りばめるのもちょっと抵抗があったので…
(あと他ユーザでログインした時に何か問題が発生するのもちょっとうーん)
今回は~/.zshrcにパス設定を書き換えるコードを追加しました。

# .zshrc

export PATH=$HOME/local/bin:$PATH

Homebrewではシンボリックリンクを/usr/local配下に置くことで対応しているようなので、もしかするとこの方が色々とやりやすいかもしれません。

% ll /usr/local/bin/python3
lrwxr-xr-x  1 user  admin    39B  5 23 09:11 /usr/local/bin/python3 -> ../Cellar/python@3.9/3.9.13/bin/python3

なんかちょっとやだなあ

なーんというか、正直あんまり綺麗じゃないなあ というのが正直なところがあり…
これがmacOSの方針であり、Appleの方針である以上我々はそれに従うしかないのはそれはそうですが、シェルの動作くらい自分で完全に決めさせていただきたいものです。

(もし何か良い方法ありましたらTwitterで教えてください)

いっそOSから作ってしまおうか。 そんな思いが頭の片隅にpushされるトラブルなのでした。

fflush(stdout)