Bash や Zsh で PATH に同じパスが何度も追加されないようにする

GNU Screen を主に使っていますが、ふと気づくと `PATH` 環境変数の中身がかなり重複してカオスになっていたので、整理しました。

環境は MacOSX と CentOS 6.x です。

また、その後 tmux でも試してみたので、その結果も合わせてまとめています。

※2016/4/19 追記:
Zsh on MacOSX でも動作確認しました。

## TL;DR

* tmux や screen を使っていると `~/.bashrc` や `~/.zshrc` が複数回読み込まれるのは避けられなさそう。
* Zsh なら `typeset -U path PATH` でいい。
* **[pathctl](https://github.com/key-amb/sh-pathctl)** というシェル関数のライブラリを作って重複させずにエントリの位置を調整するようにした。

## 要点をもう少し

* Bash の場合、基本的には `PATH` を更新する設定は `~/.bash_profile` に書くのがよさそう
  * `~/.bashrc` に書くとシェルが起動する度に読み込まれる。
* Zsh の場合は `~/.zshenv`, `~/.zshrc` ともシェル起動時に読み込まれるようだ。[^1]
* `xxenv` (rbenv など)の初期設定コマンドを `~/.zshenv` に書くと `/usr/bin` などの後ろになってしまったので、自作した **pathctl** を使いつつ `~/.zshrc` に記述した。

[^1]: [zshの設定ファイルの読み込み順序と使い方Tipsまとめ - Qiita](http://qiita.com/muran001/items/7b104d33f5ea3f75353f)

### 重複を避けたい方へオススメのやり方

（気にしない、という選択肢も割と現実的な気はします。）

気にする方向けには、以下のようなやり方が比較的手軽かなと思いました。

* ふつうに `export PATH=$PATH:/path/to/bin` のように追加していく
* Zsh なら `~/.zshenv` か `~/.zshrc` のどこかに `typeset -U path PATH` を書いておく
* Bash なら `~/.bashrc` の末尾で、以下のようにしてユニークになるようにする

```bash
_path=""
for _p in $(echo $PATH | tr ':' ' '); do
  case ":${_path}:" in
    *:"${_p}":* )
      ;;
    * )
      if [ "$_path" ]; then
        _path="$_path:$_p"
      else
        _path=$_p
      fi
      ;;
  esac
done
PATH=$_path

unset _p
unset _path
```

このコードは大抵のシェルで動作すると思います。
※手元の Mac では bash 3系, 4系, zsh, sh(実体はbash), dash で確認しました。

わざわざ書くのが面倒という方は pathctl を読み込んで以下のようにも書けます。

```bash
. pathctl.shrc
pathctl_uniq
```

----

要点は以上のようなところです。
以下はもう少し詳しい、各トピックについての補足です。

## 補足

### `tmux` や `screen` で複数回 `~/.bash_profile` 等が読み込まれる

`screen` x Bash でセッションを起動したばかりのときは、新しいウィンドウで再読み込みされるのは `~/.bashrc` だけでしたが、サーバからログアウトして再ログイン & attach すると、`~/.bash_profile` も再読み込みしているようでした。
ひょっとしたら環境起因の問題が有るかもしれません。

`tmux` の場合は、pane や window を生成する度に `~/.bash_profile` を読み込み直しているような挙動のようです。

`~/.zshenv`, `~/.zshrc` はいずれの場合も繰り返し読み込まれると思います。

### pathmunge と pathctl について

RedHat 系の OS なら「pathmunge[^2] という関数があるよ」とコメントで教えて頂きました。pathmunge は `/etc/profile` に定義されている関数で、対象が PATH に含まれてないときのみ、先頭か末尾にエントリを追加します。
ただ、自分の環境では `/etc/profile` の末尾に `unset -f pathmunge` されており、 `/etc/profile` を読み込んでも関数を使うことはできませんでした。

[^2]: [Linux: pathmunge Command in Shell Script](http://www.cyberciti.biz/faq/redhat-linux-pathmunge-command-in-shell-script/)

そこで、同等の機能を提供する [pathctl](https://github.com/key-amb/sh-pathctl) というシェルスクリプトを書きました。
`pathctl_push(_f) パス` で PATH 末尾に追加、 `pathctl_unshift(_f) パス` で PATH 先頭に追加します。

### `xxenv` について

`eval $(pyenv init -)` や `eval $(rbenv init -)` は内部的に `~/.(py|rb)env/shims` を `PATH` の先頭に追加することを行っているようです。
これは pathmunge 等では回避できないので、該当のパスが PATH に含まれていたら実行しない、のような if 文を書きました。
または、 `pathctl_uniq` 関数で重複を削除することもできます。

### どうしても `~/.bashrc` から読み込みたかったもの

ふつう、そういうものはあまり無いかと思いますが、自分の場合 `~/.bashrc.d/` に置いたものをまとめて読む処理を `~/.bashrc` に書いていたので、必要でした。
※シェル起動時、環境変数やシェル変数は引き継がれますが、`alias` などは引き継がれないようなので、そういうものは `~/.bashrc` から読まなければならない、と思っています。

このケースだと、`~/.bash_profile.d/` でも作って `~/.bash_profile` から読むようにしてもよかったです。

…が、上で見てきたように `~/.bash_profile` も複数回読み込まれることがあるので、結局は pathctl が必要でした。

## Zsh について参考

- [zsh: 17 Shell Builtin Commands](http://zsh.sourceforge.net/Doc/Release/Shell-Builtin-Commands.html)
- [zshのPATHの自動重複削除や余計なPATHの削除 - @znz blog](http://blog.n-z.jp/blog/2013-12-12-zsh-cleanup-path.html)

## 落ち穂拾い

screen を使っているとき、`~/.bash_profile` 側でガード変数を使って `~/.bashrc` を2回以上ロードしないように…というのもやってみました。
が、この場合 screen のセッションで `~/.bashrc` が読み込まれなくなり、意図した挙動にはなりませんでした。

## 脚注