2010-12-28

ZSH のパス設定

以前、MacBook と Mac mini Server の間で git clone しようとして失敗したことがあった(→「MacBook にも Git を (あるいはリモートリポジトリの準備)」)。原因は、リモート側のシェルの PATH 設定だった。git でリモートリポジトリから SSH 経由で clone しようとする場合、リモート側で(SSH 経由で) git コマンドが実行される。リモート側のシェルのパスに git コマンドをふくむディレクトリが入っていなければ、git コマンドが見つからずエラーになる。

シェルは起動時に設定ファイルを読み込む(実行する)ことで実行環境を整える。このとき、たいていのシェルでは実行モードによって読み込む設定ファイルが変わる。実行モードというのはログインシェル、対話シェル、そしてスクリプト実行用シェルの 3 つのことで、git clone で起動されるシェルはスクリプト実行用シェルになる。このため、たとえ ssh コマンドでリモートログインした状態で対話的に git コマンドを使えたとしても、git clone しようとすると (git コマンドが見つからず)エラーになることがある。

今日も、MacBook と iMac の間で git clone しようとして同じエラーが出て失敗した。以前解決したはずの問題にまた遭遇したことになる。というのも、あれからしばらくしてシェルを zsh に変えたからだ(→「twitter より (2010-08-03)」)。最初の設定のときに、ssh コマンドでリモートログインしてローカルなシェルの場合と同様に使えることを確認して安心していた。今日まで git clone のようなリモートのコマンドを直接実行することはなかったから、この問題に気付かなかった。

では、zsh を使う場合リモート(のスクリプト実行用)のシェルが参照するパスを設定するにはどうすれば良いのか? より具体的に言えば、git のリモートリポジトリを置いたコンピュータのシェルが zsh のとき、git clone 等が失敗しないようにするにはどうすれば良いのか?

答えは ~/.zshenv

結論を先に書くと、スクリプト用シェルのために PATH を設定するには ~/.zshenv に書けば良い。書式は通常のシェルスクリプトと同じ。つまり、git clone が失敗する問題を解決するためには、リモート側(clone されるリポジトリがある方)に ~/.zshenv を作り、以下のように PATH の設定をしてやれば良い(git コマンドを MacPorts でインストールしている場合)。

export PATH=/opt/local/bin:/opt/local/sbin:$PATH

これまで、zsh のためのパスは ~/.zshrc で設定してきた。この方法どこが悪かったのだろう?

それは実行モードごとの設定ファイルの読み込みが以下のようになっているからだ(→「【コラム】漢のzsh (1) 最強のシェル、それは「zsh」 | マイコミジャーナル」参照)。一言で言えば、~/.zshrc はスクリプト実行用シェルのときは読み込まれない。

zsh の設定ファイル読み込み順序
実行モード 読み込み順
ログインシェル
  1. ~/.zshenv
  2. ~/.zprofile
  3. ~/.zshrc
  4. ~/.zlogin
対話シェル
  1. ~/.zshenv
  2. ~/.zshrc
スクリプト用シェル
  1. ~/.zshevn

これを見れば、スクリプト用シェルが参照する PATH は ~/.zshenv で設定しなければならないことがわかる。

~/.zshenv を読むより前に起きていること

さて、これで MacBook と iMac の間で git clone に失敗する問題は解決できたが、ついでに zsh における PATH の設定について調べてみた。

zsh の man ページによれば、シェルが起動時に最初に読み込むのは /etc/zshenv だとのこと。その後、上述の表のような順序で設定を読み込む。では、/etc/zshenv はどうなっているのか?

Mac OS X 10.6.5 の /etc/zshenv の内容を以下に示す。

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

ここに書いてあるのは /usr/libexec/path_helper があれば、それを実行しろというもの(バッククォート記法なので、正確には path_helper の実行結果をスクリプトとして実行しろ、となる)。

この path_helper というコマンドは PATH を設定するためのシェルスクリプトを生成するためのもので -s オプションで B-shell 用のスクリプトが作られる。man ページには直接実行するものじゃないゾ、と書いてあるが、そこをあえて実行してみた結果を以下に示す。実行の前に PATH と MANPATH の設定を消しているのは、このコマンドが現在の設定に追加するようになっているから。空にしておくことで、zsh が /etc/zshenv でこのコマンドを実行したときの結果がわかる。

[imac] mnbi% PATH=''
[imac] mnbi% MANPATH=''
[imac] mnbi% /usr/libexec/path_helper -s
PATH="/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/usr/X11/bin"; export PATH;
MANPATH="/usr/share/man:/usr/local/share/man:/usr/X11/share/man"; export MANPATH;

では、path_helper は PATH (と MANPATH) に入れるべきディレクトリをどうやって知るのだろう? man ページによれば、それは /etc/paths/etc/manpaths だとのこと。そして、ここに書かれたものに、さらに /etc/paths.d/etc/manpaths.d に置かれたファイルの内容を付け加えて、PATH と MANPATH とする。

以下に、Mac OS X 10.6.5 の /etc/paths の内容を示す。

/usr/bin
/bin
/usr/sbin
/sbin
/usr/local/bin

同じく、Mac OS X 10.6.5 では /etc/paths.d には X11 というファイルがあり、その内容は以下のようになっている。

/usr/X11/bin

以上のことから、~/.zshenv を使ってユーザごとにパスを設定する以外にも、追加したいパスを書いたファイルを /etc/paths.d に置けば、システム全体で(正確には zsh を使っているすべてのユーザで)同様の効果が得られることがわかる。

関連リンク

関連記事

2010-12-27

NSTableView でソートしたとき、選択された要素をモデル配列から正しく取り出す方法

NSTableView では、見出し行をクリックするとカラムの内容によって行をソートすることができる。ところが、これはモデル(となっている NSMutableArray)をソートしているわけではないから、ソートしたビュー上の並び順とモデル内の要素の並び順は一致していない。

たとえば、もともと「りんご」「みかん」「ぶどう」の順にモデル配列の中に収められているとして、これをビュー上でソートして「ぶどう」「みかん」「りんご」の順に表示されているとする。ここでビュー上で「りんご」を選択すると、そのインデックスは(0 から数えるので) 2 となる。このインデックスを使い、モデル配列から objectAtIndex: で要素を取り出せば、それは「りんご」ではなく「ぶどう」になってしまう。

つまり、ビューでソートした状態でビュー上の選択(のインデックス)に合わせてモデルの要素を取り出すと、ビューの選択とは異なる要素を取り出してしまうことになるのだ。

MacBloggerGlass の場合で言うと、記事の一覧をタイトルまたは日付けでソートした状態で一覧から記事の選択を行うと、表示される記事の内容がずれてしまう。

こうしたビューとモデルにおける要素の並び順の不一致を避けるには、両者を結びつけている NSArrayController を使えば良い。

具体的には、NSArrayController から selectedObjects で要素オブジェクト(の配列)を取り出せば、ビューでソートしているかどうかに関係なく、適切なオブジェクト(つまりビュー上の選択と同じオブジェクト)が取り出せる。

MacBloggerGlass の場合、以下のようなコードを使う(AppController.m より抜粋)。

    Entry *entry = [[feedController selectedObjects] objectAtIndex:0];

feedController が記事一覧(と記事内容)用のモデル配列のための NSArrayController だ。AppController の IBOutlet として確保し、Interface Builder で NSArrayController のインスタンスと結びつけている。記事一覧では複数選択を許していないから、selectedObjects の返す配列の最初の要素を取り出している。

ちなみに、この NSArrayController を使う解決方法はググって見つけた(→Cocoaの日々: NSArrayController を使った NSTableView で選択行の情報を取得する)

NStableView と NSArrayController の関係

では、なぜ NSArrayController からは、ソートの状態に関係なく、適切なモデルオブジェクトを取り出すことができるのだろうか?

オブジェクトの配列をモデルとして、その内容を NSTableView に表示させる場合(配列の要素になっているオブジェクトのプロパティを表のカラムに表示させる、という意味)、Cocoa Bindings では両者を NSArrayController で結びつける。

これは詳細に言えば、NSTableColumn の値として、NSArrayController の arrangedObjects が返す配列の要素のプロパティを結びつける、ということだ。初期状態では、arrangedObjects はモデルの配列の並び順そのままの配列を返す。だから、モデル配列の順序がそのままビューの表示順序になる。一方、NSTableView の見出し行でソートしたときは、NSArrayController の arrangedObjects が返す配列の並び順が変わる。その結果、ビューの表示順序が変わる。

実はビュー(NSTableView)の行がソートされるのはむしろ結果で、実際にはコントローラ(NSArrayController)が返す要素の順序がソートされていたのだ。これは、NSArrayController がソートされた状態のモデル配列を保持している、と言っても良い。だから、ビュー上でソートされた状態のインデックスを使っても、適切なオブジェクトを取り出すことができるのだ。

では、なぜ NSArrayController の arrangedObjects の並び順が変わるのか? それは、NSTableView の見出し行がクリックされたときに NSArrayController に対して setSortDescriptors が呼ばれ、NSSortDescriptor がセットされることによる。これはソートの方法を指示するオブジェクトで、NSArrayController は arrangedObjects を作り出す際に参照するものだ。初期状態ではこれがセットされていないため(ソートされず)、arrangedObjects はモデルの要素順と同じ順序の配列になっている。

  • 見出しをクリックしてからの一連の動きを時系列で並べると以下のようになる。
    1. NSTableView の見出しがクリックされる。
    2. NSTableView が NSArrayController に対して、NSSortDescriptor をセットする。
    3. NSTableView (実際には NSTableColumn) が NSArrayController に対して arrangedObjects を要求する(メソッドを呼び出す)。
    4. NSArrayController は先にセットされた NSSortDescriptor にしたがってソートした配列を返す。

    まとめると、ソートはビューで起きているのではなく、コントローラの内部で起きている、ということになる。だから、コントローラはソートされた状態のインデックスから適切なオブジェクトを取り出すことができる、と。

    iOS アプリではどうする?

    Cocoa Bindings が使えない iOS アプリでは NSArrayController に相当する部分を自前で用意しなければならないはず。なんだか面倒なことになりそうな予感がする。ソートしなければ(できないようにすれば)良いんだけど。そもそも、iOS のアプリで表をソートするアプリって見かけないような……。

    関連リンク

    関連記事