1. Qiita
  2. 投稿
  3. Bash

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

  • 14
    いいね
  • 3
    コメント

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 というシェル関数のライブラリを作って重複させずにエントリの位置を調整するようにした。

要点をもう少し

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

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

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

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

  • ふつうに export PATH=$PATH:/path/to/bin のように追加していく
  • Zsh なら ~/.zshenv~/.zshrc のどこかに typeset -U path PATH を書いておく
  • Bash なら ~/.bashrc の末尾で、以下のようにしてユニークになるようにする
_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 を読み込んで以下のようにも書けます。

. pathctl.shrc
pathctl_uniq

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

補足

tmuxscreen で複数回 ~/.bash_profile 等が読み込まれる

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

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

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

pathmunge と pathctl について

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

そこで、同等の機能を提供する pathctl というシェルスクリプトを書きました。
pathctl_push(_f) パス で PATH 末尾に追加、 pathctl_unshift(_f) パス で PATH 先頭に追加します。

xxenv について

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

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

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

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

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

Zsh について参考

落ち穂拾い

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

脚注