よんちゅBlog

― このブログは自分用のメモや日々の問題などを共有するためのものです ―

20121005185841 お知らせ:  2013/07/17 ブログデザインをリニューアルしました。

【Mac】zshをサブシェルで起動するとPATHがおかしくなる

どういう状態かというと。

★サブシェル起動前
$ printpath    ← $PATHを表示
/Users/yonchu/.pythonbrew/bin
/Users/yonchu/.rvm/gems/ruby-1.9.3-p286/bin
/Users/yonchu/.rvm/gems/ruby-1.9.3-p286@global/bin
/Users/yonchu/.rvm/rubies/ruby-1.9.3-p286/bin
/Users/yonchu/.rvm/bin
/usr/local/share/python
/usr/local/mysql/bin
/Library/Java/Home/bin
/Users/yonchu/bin
/Users/yonchu/dotfiles/bin
/usr/local/bin
/usr/local/sbin
/usr/local/share
/usr/bin
/bin
/usr/sbin
/sbin
/usr/X11/bin
$
$ zsh    ← サブシェルとして zsh を起動
$
★サブシェル起動後
$ printpath    ← $PATHを表示
/Users/yonchu/.pythonbrew/bin
/Users/yonchu/.rvm/gems/ruby-1.9.3-p286/bin
/Users/yonchu/.rvm/gems/ruby-1.9.3-p286@global/bin
/Users/yonchu/.rvm/rubies/ruby-1.9.3-p286/bin
/Users/yonchu/.rvm/bin
/usr/bin
/bin
/usr/sbin
/sbin
/usr/local/bin
/usr/X11/bin
/usr/local/share/python
/usr/local/mysql/bin
/Library/Java/Home/bin
/Users/yonchu/bin
/Users/yonchu/dotfiles/bin
/usr/local/sbin
/usr/local/share

(※ printpathはPATHを整形して出力してるだけです)

という感じです。
意味分かりませんね。

原因

何が問題なのかというと、/etc/zshenv に致命的な誤りがありました。
普段ユーザが触れるようなファイルではないので、え?と思うかもしれません。

この問題の原因に対して、homebrewのzshで以下のような注意書きがありました。

$ brew info zsh
zsh: stable 5.0.0
http://www.zsh.org/
Depends on: gdbm, pcre
/usr/local/Cellar/zsh/5.0.0 (956 files, 9.1M) *
https://github.com/mxcl/homebrew/commits/master/Library/Formula/zsh.rb
==> Options
--disable-etcdir
        Disable the reading of Zsh rc files in /etc
==> Caveats
To use this build of Zsh as your login shell, add it to /etc/shells.

If you have administrator privileges, you must fix an Apple miss
configuration in Mac OS X 10.7 Lion by renaming /etc/zshenv to
/etc/zprofile, or Zsh will have the wrong PATH when executed
non-interactively by scripts.

Alternatively, install Zsh with /etc disabled:

  brew install --disable-etcdir zsh

homebrewでzshを入れている方はこの注意書きに気づいたでしょうか。
自分は全然気づきませんでした…

対処方法

説明に書かれている通り、対処方法は2種類

  • /etc/zshenv を /etc/zprofile にリネームする
  • brew install --disable-etcdir zsh で/etc 無効版をインストールする

今回私は1を採用しました。
(1の方が今後気にすることが少なそうなので)

$ ls -l /etc/zshenv
-r--r--r--   1 root wheel  126 2012-04-06 03:56 zshenv
$ sudo mv /etc/zshenv /etc/zprofile
$ ls -l /etc/zprofile
-r--r--r--   1 root wheel  126 2012-04-06 03:56 zprofile
以下、自分用のメモを兼ねて説明

まず、zsh起動時に読み込まれる設定ファイルの読み込み順序についてです。
zshの設定ファイルは種類が多いので非常に分かりづらいです。

zshを起動する方法によって以下のように分けられます。

ログインシェルとして起動した場合

 /etc/zshenv
 $ZDOTDIR/.zshenv
 /etc/zprofile
 $ZDOTDIR/.zprofile
 /etc/zshrc
 $ZDOTDIR/.zshrc
 /etc/zlogin
 $ZDOTDIRA/.zlogin

インタラクティブシェル(対話シェル)として起動した場合
(GUIターミナル/ssh/サブシェルなど)
(※ MacのGUIターミナルはログインシェル扱いみたいです)

 /etc/zshenv
 $ZDOTDIR/.zshenv
 /etc/zshrc
 $ZDOTDIR/.zshrc

ノンインタラクティブシェル(非対話シェル)として起動した場合
(シェルスクリプト/リモートシェルなど)

 /etc/zshenv
 $ZDOTDIR/.zshenv

※ $ZDOTDIRを設定していない場合は、$HOMEになります。

今回問題となったのはサブシェルなので2番目のケースですね。

続いて問題の /etc/zshenv の中身です。

$ cat /etc/zshenv
# system-wide environment settings for zsh(1)
if [ -x /usr/libexec/path_helper ]; then
        eval `/usr/libexec/path_helper -s`
fi

path_helper というコマンドを起動しているだけのようです。

path_helper というのは man によると、

DESCRIPTION
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.  (The MANPATH environment variable will not be modified unless it is already set in the environment.)

     Files in these directories should contain one path element per line.

     Prior to reading these directories, default PATH and MANPATH values are obtained from the files /etc/paths and /etc/manpaths respectively.

     Options:

     -c      Generate C-shell commands on stdout.  This is the default if SHELL ends with "csh".

     -s      Generate Bourne shell commands on stdout.  This is the default if SHELL does not end with "csh".

ということだそうです。

要するにデフォルトのPATHを設定しますってことですね。
デフォルトで設定されるパスは、/etc/paths に書かれているパスと /etc/paths.d 配下にあるファイルの中に書かれているパスです。

私のMac(OS X 10.7)では以下のようなパスが書かれていました。

$ cat /etc/paths
/usr/bin
/bin
/usr/sbin
/sbin
/usr/local/bin

$ cat /etc/paths.d/*
/usr/X11/bin

ここで私のPATH設定環境について補足しておくと、
基本的に、PATHは ZDOTDIR/.zprofile で設定しています。
しかし、一部 pythonbrew や rvm などのPATH設定処理を含んでいるスクリプトを $ZDOTDIR/.zshrc で読み込んでいます。

サブシェル起動時のPATH設定の流れ

以上を踏まえて、サブシェル起動時にどのようにPATHが設定されているかを考えてみます。
おそらく以下のような流れになっていると思われます。

  1. /etc/zshenv がデフォルトパスを設定
  2. 親シェルで元々設定されていたPATHを設定(環境変数の引き継ぎ)
  3. ZDOTDIR/.zshrc に書かれているPATHを設定

ここで1つ注意すべき点があります。
PATHを設定する際、重複したパスが存在すると"左"に書かれているパスが優先して設定され、"右"にあるパスは削除されます。
この重複パスを削除する機能は、通常 "typeset -U path" を行なっていないと有効になりませんが、2番目の親シェルから引き継いだパスを設定する際は、"typeset -U" を行なっていなくても重複パスが削除されるようになっています。

この重複削除によって、一見なぜあんな並び方になったのかが分かりづらい状態になってしまったわけです。

また、.zprofile や .zlogin を使用せず、PATH設定を全て .zshrc で行なっている場合は、サブシェル起動時でも .zshrc でPATHを上書きできるため問題が表面化しづらくなっています。
しかし、/etc/zshenv はサブシェルなどのインタラクティブシェルだけでなく、通常のスクリプトをzshで起動した場合にも読み込まれてしまうため、問題になる可能性があります。
(シェバングでzshを指定している場合や、zshを指定してスクリプトを起動する場合など)

Macでzshを使用している方はお気をつけ下さい。